diff --git a/.browserslistrc b/.browserslistrc index ea5c29134f6fc..a26ad8491dfa0 100644 --- a/.browserslistrc +++ b/.browserslistrc @@ -1,4 +1,13 @@ # https://github.com/browserslist/browserslist#readme +defaults and supports es6-module +maintained node versions + +[production] + +cover 95% +not dead + +[development] + defaults -Explorer >= 10 diff --git a/.editorconfig b/.editorconfig index e85c3fc5982fe..c6c8b3621938a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,13 +1,9 @@ -# https://editorconfig.org/ - root = true [*] indent_style = space indent_size = 2 end_of_line = lf -insert_final_newline = true +charset = utf-8 trim_trailing_whitespace = true - -[*.svg] -insert_final_newline = false +insert_final_newline = true diff --git a/.eslintignore b/.eslintignore index 0a78c7ae867ad..0462061a249c3 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,4 +1,29 @@ -**/*.min.js -static/legacy/ -external/ -build/ +node_modules + +# Next.js & Vercel Directories +.next +.turbo +.swc +build + +# Files that should not be parsed +CODEOWNERS + +# Legacy Public Files +public/en/user-survey-report +public/static/documents +public/static/legacy + +# We don't want to lint/prettify the Coverage Results +coverage + +# MDX Plugin enforces Prettier formatting which should +# be done in the future as we don't want to update the Markdown file +# contents right now +pages/**/*.md + +# We shouldn't lint statically generated Storybook files +storybook-static + +# This file naturally might break conventional rules +global.d.ts diff --git a/.eslintrc b/.eslintrc index a7d672df29d7a..0f5fd7d42380e 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,51 +1,44 @@ { "extends": [ - "eslint:recommended", - "standard", - "prettier" + "next", + "next/core-web-vitals", + "prettier", + "plugin:storybook/recommended" ], - "plugins": [ - "prettier" - ], - "rules": { - "prettier/prettier": "error" - }, "overrides": [ { - "files": [ - "**/*.md" - ], - "plugins": [ - "markdown" - ], - "processor": "markdown/markdown" + "files": ["**/*.ts?(x)"], + "plugins": ["@typescript-eslint"], + "rules": { + "@typescript-eslint/consistent-type-imports": "error" + } + }, + { + "files": ["**/*.md?(x)"], + "extends": ["plugin:mdx/recommended"] }, { - "files": [ - "**/*.md/*.js" - ], - "parserOptions": { - "ecmaVersion": "latest" - }, + "files": ["**/*.{mdx,tsx}"], "rules": { - "eqeqeq": "off", - "n/no-deprecated-api": "off", - "n/handle-callback-err": "off", - "no-const-assign": "off", - "no-undef": "off", - "no-unused-expressions": "off", - "no-unused-vars": "off", - "node/handle-callback-err": "off", - "node/no-deprecated-api": "off", - "prefer-const": "off", - "prettier/prettier": [ + "react/function-component-definition": [ "error", { - "singleQuote": true, - "trailingComma": "none" + "namedComponents": "arrow-function", + "unnamedComponents": "arrow-function" + } + ], + "no-restricted-syntax": [ + "error", + { + "selector": "ImportDeclaration[source.value='react'][specifiers.0.type='ImportDefaultSpecifier']", + "message": "Default React import not allowed since we use the TypeScript jsx-transform. If you need a global type that collides with a React named export (such as `MouseEvent`), try using `globalThis.MouseHandler`" + }, + { + "selector": "ImportDeclaration[source.value='react'] :matches(ImportNamespaceSpecifier)", + "message": "Named * React import is not allowed. Please import what you need from React with Named Imports" } ], - "semi": ["error", "always"] + "@typescript-eslint/consistent-type-definitions": ["error", "type"] } } ] diff --git a/.github/ISSUE_TEMPLATE/01-bug-report.yml b/.github/ISSUE_TEMPLATE/01-bug-report.yml index 4296d9ab9c8cd..281e5e0159968 100644 --- a/.github/ISSUE_TEMPLATE/01-bug-report.yml +++ b/.github/ISSUE_TEMPLATE/01-bug-report.yml @@ -1,50 +1,49 @@ -name: 🐛 Bug Report For Nodejs Website -description: Report an issue if something isn't working as expected 🤔. +name: Report a Technical/Visual Issue on the Node.js Website +description: 'Is something not working as expected? Did you encounter a glitch or a bug with the Website?' labels: [bug] body: - type: markdown attributes: value: | - Thanks for wanting to report an issue you've found on the nodejs.org website. - Please fill in the template below. If unsure about something, just do as best - as you're able. If you are reporting a visual glitch, it will be much easier + Thanks for reporting an issue you've found on the nodejs.org website. + Please fill in the template below. If unsure about something, just do as best + as you're able. If you are reporting a visual glitch, it will be much easier for us to fix it when you attach a screenshot as well. - type: input attributes: - label: "URL:" + label: 'URL:' description: The URL of the page you are reporting an issue on. placeholder: https://nodejs.org/en/ validations: required: true - type: input attributes: - label: "Browser Name:" - description: What kind of browser are you using? + label: 'Browser Name:' + description: What kind of browser are you using? placeholder: Chrome validations: required: true - type: input attributes: - label: "Browser Version:" + label: 'Browser Version:' description: What version of browser are you using? - placeholder: "103.0.5060.134" + placeholder: '103.0.5060.134' validations: required: true - type: input attributes: - label: "Operation System:" - description: - What kind of operation system are you using + label: 'Operating System:' + description: What kind of operation system are you using (Write it in full, with version number)? - placeholder: "Windows 10, 21H2, 19044.1826" + placeholder: 'Windows 10, 21H2, 19044.1826' validations: required: true - type: textarea attributes: - label: "How to reproduce the issue:" + label: 'How to reproduce the issue:' placeholder: | - 1. What I did. - 2. What I expected to happen. + 1. What I did. + 2. What I expected to happen. 3. What I actually got. 4. If possible, images or videos are welcome. validations: diff --git a/.github/ISSUE_TEMPLATE/02-feature-request.yml b/.github/ISSUE_TEMPLATE/02-feature-request.yml index 4ba32b7ac8354..da59bf9dbc09b 100644 --- a/.github/ISSUE_TEMPLATE/02-feature-request.yml +++ b/.github/ISSUE_TEMPLATE/02-feature-request.yml @@ -1,5 +1,5 @@ -name: 🚀 Feature Request For Nodejs Website -description: I have a suggestion (and may want to implement it)! +name: Suggest a new feature or improvement for the Node.js Website +description: 'Do you have an idea or a suggestion and you want to share?' labels: [feature request] body: - type: markdown @@ -8,11 +8,9 @@ body: You have an idea how to improve the site? That's awesome! Before submitting, please have a look at the existing issues if there's already something related to your suggestion. - We are also working on a relaunch at the moment, - so it might be a good idea to check out our plans there as well: https://github.com/nodejs/nodejs.dev/issues/. - type: textarea attributes: - label: "Enter your suggestions in details:" + label: 'Enter your suggestions in details:' placeholder: | 1. What I expected to happen. 2. Your reason (if possible, images or videos are welcome). diff --git a/.github/ISSUE_TEMPLATE/03-i18n.yml b/.github/ISSUE_TEMPLATE/03-i18n.yml deleted file mode 100644 index 6ad3d9daf672c..0000000000000 --- a/.github/ISSUE_TEMPLATE/03-i18n.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: 🔡 Internationalization and translations For Nodejs Website -description: Hello. Hola. Salut. Ciao. Здравствуйте. こんにちは. مرحباً -labels: [i18n] -body: - - type: markdown - attributes: - value: | - Please mention the relevant localization team in your issue, - it helps us having the right people take a look, or if you want - to create a new localization team. - - type: textarea - attributes: - label: "Enter your issue on localizations here:" - placeholder: | - 1. The name of the team (e.g: nodejs/node-cn). - 2. Any problems to report or you want to create it? - validations: - required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 60eef11252dd9..5470b5cc09c8f 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,8 +1,11 @@ -blank_issues_enabled: false +blank_issues_enabled: true contact_links: - - name: ⁉️ Need help with Node.js? - url: https://github.com/nodejs/help/issues/ - about: File an issue in our help repo. - - name: 📗 Node.js API Docs + - name: Report an API Docs Issue on the Node.js Website url: https://github.com/nodejs/node/issues/new?assignees=&labels=doc&template=3-api-ref-docs-problem.yml - about: Please open an issue in the main Node.js repo. + about: 'Is something wrong with the API Docs? Did you face a bug with the API Docs?' + - name: Report a Translation Issue on the Node.js Website + url: https://crowdin.com/project/nodejs-website + about: 'Is something wrong in a specific translation? Do you believe a language can get improved? Do you have suggestions?' + - name: Need help with Node.js? + url: https://github.com/nodejs/help/issues/ + about: "Struggling with Node.js? You're not sure how to code?" diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000000000..bd4ff1cd9eeea --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,27 @@ + + +## Description + + + +## Related Issues + + + +### Check List + + + +- [ ] I have read the [Contributing Guidelines](https://github.com/nodejs/nodejs.org/blob/main/CONTRIBUTING.md) and made commit messages that follow the guideline. +- [ ] I have run `npx turbo lint` to ensure the code follows the style guide. And run `npx turbo lint:fix` to fix the style errors if necessary. +- [ ] I have run `npx turbo format` to ensure the code follows the style guide. +- [ ] I have run `npx turbo test` to check if all tests are passing, and/or `npx turbo test:snapshot` to update snapshots if I created and/or updated React Components. +- [ ] I've covered new added functionality with unit tests if necessary. diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 774051b2278c7..f3c5e7a4f9f47 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,12 +1,42 @@ version: 2 updates: - package-ecosystem: github-actions - directory: "/" + directory: '/' schedule: - interval: weekly + interval: monthly open-pull-requests-limit: 10 - package-ecosystem: npm - directory: "/" + directory: / schedule: - interval: weekly + interval: monthly + commit-message: + prefix: meta + groups: + storybook: + patterns: + - 'storybook' + - '@storybook/*' + testing-library: + patterns: + - '@testing-library/*' + - '@types/testing-library*' + next-js: + patterns: + - 'next' + - 'turbo' + - 'next-mdx-remote' + - 'next-sitemap' + - 'next-themes' + - '@vercel/*' + remark-mdx: + patterns: + - '@vcarl/remark-headings' + - 'remark-gfm' + - '@mdx-js/*' + react: + patterns: + - 'react' + - 'react-dom' + - '@types/react' + - '@types/react-dom' open-pull-requests-limit: 10 diff --git a/.github/workflows/build-and-analysis.yml b/.github/workflows/build-and-analysis.yml new file mode 100644 index 0000000000000..e91be44c10e69 --- /dev/null +++ b/.github/workflows/build-and-analysis.yml @@ -0,0 +1,160 @@ +name: Build and Analysis Checks + +on: + push: + branches: + - main + pull_request_target: + branches: + - main + workflow_dispatch: + +defaults: + run: + working-directory: ./ + +permissions: + contents: read + actions: read + pull-requests: write + +jobs: + build: + name: Build on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest] + + steps: + - name: 'Use GNU tar instead BSD tar' + if: matrix.os == 'windows-latest' + shell: cmd + run: echo C:\Program Files\Git\usr\bin>>"%GITHUB_PATH%" + + - name: Git Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 2 + + - name: Restore Cache + uses: actions/cache/restore@v3 + with: + path: | + ~/.npm + .next/cache + node_modules/.cache + key: cache-${{ hashFiles('package-lock.json') }} + restore-keys: cache- + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version-file: '.nvmrc' + cache: 'npm' + + - name: Install NPM packages + run: npm ci --no-audit --no-fund --omit=dev + + - name: Build Next.js + run: npx turbo build + env: + TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} + TURBO_TEAM: ${{ secrets.TURBO_TEAM }} + NODE_OPTIONS: '--max_old_space_size=4096' + NEXT_TELEMETRY_DISABLED: 1 + + - name: Analyse Build + run: npx -p nextjs-bundle-analysis report + + - name: Upload Build Analysis + uses: actions/upload-artifact@v3 + with: + name: bundle-analysis + path: .next/analyze/__bundle_analysis.json + + - name: Save Cache + uses: actions/cache/save@v3 + with: + path: | + ~/.npm + .next/cache + node_modules/.cache + key: cache-${{ hashFiles('package-lock.json') }} + + analysis: + name: Analysis + runs-on: ubuntu-latest + needs: build + + steps: + - name: Git Checkout + uses: actions/checkout@v3 + + - name: Restore Cache + uses: actions/cache/restore@v3 + with: + path: | + ~/.npm + .next/cache + node_modules/.cache + key: cache-${{ hashFiles('package-lock.json') }} + restore-keys: cache- + + - name: Download PR Bundle Analysis + uses: actions/download-artifact@v3 + with: + name: bundle-analysis + path: .next/analyze + + - name: Copy PR Bundle Analysis (Fallback) + run: | + mkdir -p .next/analyze/base/bundle/ + cp .next/analyze/__bundle_analysis.json .next/analyze/base/bundle/__bundle_analysis.json + + - name: Download Base Bundle Analysis + uses: dawidd6/action-download-artifact@v2 + if: success() && github.event.number + with: + name: bundle-analysis + branch: ${{ github.event.pull_request.base.ref }} + path: .next/analyze/base/bundle + if_no_artifact_found: warn + + - name: Compare with base branch bundle + if: success() && github.event.number + run: ls -laR .next/analyze/base && npx -p nextjs-bundle-analysis compare + + - name: Get Comment Body + id: get-comment-body + if: success() && github.event.number + run: | + echo "body<> $GITHUB_OUTPUT + echo "$(cat .next/analyze/__bundle_analysis_comment.txt)" >> $GITHUB_OUTPUT + echo EOF >> $GITHUB_OUTPUT + + - name: Find Comment + uses: peter-evans/find-comment@v2 + if: success() && github.event.number + id: find-comment-id + with: + issue-number: ${{ github.event.number }} + body-includes: '' + + - name: Create Comment + uses: peter-evans/create-or-update-comment@v2 + if: success() && github.event.number && steps.find-comment-id.outputs.comment-id == 0 + with: + issue-number: ${{ github.event.number }} + body: ${{ steps.get-comment-body.outputs.body }} + + - name: Update Comment + uses: peter-evans/create-or-update-comment@v2 + if: success() && github.event.number && steps.find-comment-id.outputs.comment-id != 0 + with: + issue-number: ${{ github.event.number }} + body: ${{ steps.get-comment-body.outputs.body }} + comment-id: ${{ steps.find-comment-id.outputs.comment-id }} + edit-mode: replace diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 5dada5c8f742c..0000000000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,76 +0,0 @@ -name: CI - -on: - push: - branches: - - main - pull_request: - workflow_dispatch: - -env: - FORCE_COLOR: 2 - -jobs: - lint: - name: Lint - runs-on: ubuntu-latest - - steps: - - name: Clone repository - uses: actions/checkout@v3 - - - name: Set up Node.js - uses: actions/setup-node@v3 - with: - node-version: "lts/*" - cache: npm - - - name: Install npm dependencies - run: npm ci - - - name: Lint - run: | - echo "::add-matcher::.github/workflows/remark-lint-problem-matcher.json" - npm run test:lint - - test: - name: Node on ${{ matrix.os }} - runs-on: ${{ matrix.os }} - - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, windows-latest] - - steps: - - name: Clone repository - uses: actions/checkout@v3 - - - name: Set up Node.js - uses: actions/setup-node@v3 - with: - node-version: "lts/*" - cache: npm - - - run: java -version - - - name: Install npm dependencies - run: npm ci - - - name: Build - run: npm run build - - - name: Run unit tests - run: npm run test:unit - - - name: Run HTML validator - run: npm run test:html - - - name: Run linkinator - uses: JustinBeckwith/linkinator-action@v1 - with: - linksToSkip: "^(?!http://localhost)" - paths: en - recurse: true - serverRoot: build - verbosity: error diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml deleted file mode 100644 index 93e877256c86b..0000000000000 --- a/.github/workflows/codeql.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: "CodeQL" - -on: - push: - branches: - - main - pull_request: - branches: - - main - schedule: - - cron: "0 0 * * 0" - workflow_dispatch: - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - - steps: - - name: Checkout repository - uses: actions/checkout@v2 - - - name: Initialize CodeQL - uses: github/codeql-action/init@v2 - with: - languages: "javascript" - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml new file mode 100644 index 0000000000000..cf39524185cf2 --- /dev/null +++ b/.github/workflows/pull-request.yml @@ -0,0 +1,108 @@ +name: Pull Request Checks + +on: + pull_request_target: + branches: + - main + workflow_dispatch: + +defaults: + run: + working-directory: ./ + +permissions: + contents: read + actions: read + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + + steps: + - name: Git Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 2 + + - name: Restore Cache + uses: actions/cache/restore@v3 + with: + path: | + ~/.npm + .next/cache + node_modules/.cache + key: cache-${{ hashFiles('package-lock.json') }}- + restore-keys: cache- + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version-file: '.nvmrc' + cache: 'npm' + + - name: Install NPM packages + run: npm ci --no-audit --no-fund + + - name: Run Linting + run: npx turbo lint + env: + TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} + TURBO_TEAM: ${{ secrets.TURBO_TEAM }} + + - name: Run Prettier + run: npx turbo prettier + env: + TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} + TURBO_TEAM: ${{ secrets.TURBO_TEAM }} + + unit-tests: + name: Tests on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest] + + steps: + - name: 'Use GNU tar instead BSD tar' + if: matrix.os == 'windows-latest' + shell: cmd + run: echo C:\Program Files\Git\usr\bin>>"%GITHUB_PATH%" + + - name: Git Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 2 + + - name: Restore Cache + uses: actions/cache/restore@v3 + with: + path: | + ~/.npm + .next/cache + node_modules/.cache + key: cache-${{ hashFiles('package-lock.json') }} + restore-keys: cache- + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version-file: '.nvmrc' + cache: 'npm' + + - name: Install NPM packages + run: npm ci --no-audit --no-fund + + - name: Run Unit Tests + run: npx turbo test:unit -- --coverage + env: + TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} + TURBO_TEAM: ${{ secrets.TURBO_TEAM }} + + - name: Run Storybook Tests + run: npx turbo test:storybook + env: + TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} + TURBO_TEAM: ${{ secrets.TURBO_TEAM }} diff --git a/.github/workflows/remark-lint-problem-matcher.json b/.github/workflows/remark-lint-problem-matcher.json deleted file mode 100644 index cfb281310a9a0..0000000000000 --- a/.github/workflows/remark-lint-problem-matcher.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "problemMatcher": [ - { - "owner": "remark-lint", - "pattern": [ - { - "regexp": "^(?:\\x1b\\[\\d+m)*(.+?)(?:\\x1b\\[\\d+m)*$", - "file": 1 - }, - { - "regexp": "^\\s+(?:\\d+:\\d+-)?(\\d+):(\\d+)\\s+\\S*(error|warning|info)\\S*\\s+(.+)\\s+(\\S+)\\s+(?:\\S+)$", - "line": 1, - "column": 2, - "severity": 3, - "message": 4, - "code": 5, - "loop": true - } - ] - } - ] -} diff --git a/.gitignore b/.gitignore index c648057343c34..f8f1a72df5ba7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,16 +1,29 @@ -# Generated HTML and other static files -build/ # Commonly ignored Node.js files -node_modules/ +node_modules npm-debug.log -.npm/ +.npm # OSX system files, the bane of our existence .DS_Store .AppleDouble .LSOverride -# Netlify -.netlify +# Next.js Build Output +.next +build -.cache/ +# Next.js Generated Files +public/robots.txt +public/sitemap.xml +public/en/feed/*.xml + +# Jest +coverage +.swc + +# Storybook +storybook-static + +# Vercel Files +.vercel +.turbo diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000000000..824fa38a75871 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,11 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +DIR=$(cd `dirname $0` && pwd -P) + +# if the generated files got tracked to this commit we revert them +git reset $DIR/../public/node-releases-data.json +git reset $DIR/../public/blog-posts-data.json + +# lint and format staged files +npx lint-staged diff --git a/.lintstagedrc b/.lintstagedrc new file mode 100644 index 0000000000000..c8516eb2555b4 --- /dev/null +++ b/.lintstagedrc @@ -0,0 +1,4 @@ +{ + "**/*.{js,mjs,ts,tsx,md,mdx}": ["eslint --fix"], + "**/*.{js,mjs,ts,tsx,md,mdx,json,yml}": ["prettier --write"] +} diff --git a/.mailmap b/.mailmap deleted file mode 100644 index ce9ed4e2b3ee1..0000000000000 --- a/.mailmap +++ /dev/null @@ -1 +0,0 @@ -Mary Marchini diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000000000..a77793ecc5200 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +lts/hydrogen diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000000000..e3c3f6b247bd4 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,31 @@ +node_modules + +# Next.js & Vercel Directories +.next +.turbo +.swc +build + +# Files that should not be parsed +CODEOWNERS + +# Legacy Public Files +public/en/user-survey-report +public/static/documents +public/static/legacy +public/node-releases-data.json + +# We don't want to lint/prettify the Coverage Results +coverage + +# MDX Plugin enforces Prettier formatting which should +# be done in the future as we don't want to update the Markdown file +# contents right now +pages/**/*.md + +# Prettier's Handlebar parser is limited and chokes on some syntax features +# https://github.com/prettier/prettier/issues/11834 +scripts/release-post/template.hbs + +# We shouldn't lint statically generated Storybook files +storybook-static diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000000000..867a6097eeb5d --- /dev/null +++ b/.prettierrc @@ -0,0 +1,12 @@ +{ + "printWidth": 80, + "tabWidth": 2, + "useTabs": false, + "semi": true, + "singleQuote": true, + "jsxSingleQuote": false, + "trailingComma": "es5", + "bracketSpacing": true, + "bracketSameLine": false, + "arrowParens": "avoid" +} diff --git a/.prettierrc.json b/.prettierrc.json deleted file mode 100644 index 0fbddb05993d9..0000000000000 --- a/.prettierrc.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "overrides": [ - { - "files":[ - "**/*.js" - ], - "options": { - "singleQuote": true, - "trailingComma": "none" - } - } - ] -} diff --git a/.remarkignore b/.remarkignore deleted file mode 100644 index 2b569d27b58d7..0000000000000 --- a/.remarkignore +++ /dev/null @@ -1,4 +0,0 @@ -# We don't need to check all the md files under 'test/scripts' -# Because they are for test ONLY - -tests/scripts/ diff --git a/.remarkrc b/.remarkrc index 89bb6a54f34e8..e908e885412b6 100644 --- a/.remarkrc +++ b/.remarkrc @@ -1,7 +1,7 @@ { "plugins": [ - "remark-frontmatter", "remark-preset-lint-node", + "preset-prettier", ["remark-lint-fenced-code-flag", false], ["remark-lint-first-heading-level", false], ["remark-lint-maximum-line-length", false], diff --git a/.storybook/main.ts b/.storybook/main.ts new file mode 100644 index 0000000000000..840be093886fd --- /dev/null +++ b/.storybook/main.ts @@ -0,0 +1,13 @@ +import type { StorybookConfig } from '@storybook/nextjs'; + +const config: StorybookConfig = { + stories: ['../components/**/*.stories.tsx'], + addons: ['@storybook/addon-controls', '@storybook/addon-interactions'], + framework: { name: '@storybook/nextjs', options: {} }, + features: { storyStoreV7: true }, + docs: { autodocs: 'tag' }, + staticDirs: ['../public'], + core: { disableTelemetry: true }, +}; + +export default config; diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx new file mode 100644 index 0000000000000..5bbead7110c2f --- /dev/null +++ b/.storybook/preview.tsx @@ -0,0 +1,47 @@ +import NextImage from 'next/image'; +import { SiteProvider } from '../providers/siteProvider'; +import { ThemeProvider } from '../providers/themeProvider'; +import { LocaleProvider } from '../providers/localeProvider'; +import { openSans } from '../util/nextFonts'; +import type { Preview } from '@storybook/react'; + +import '../styles/index.scss'; + +const preview: Preview = { + parameters: { + actions: { argTypesRegex: '^on[A-Z].*' }, + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/, + }, + }, + nextjs: { + router: { + basePath: '', + }, + }, + backgrounds: { disable: true }, + }, +}; + +export const decorators = [ + Story => ( + + + +
+ +
+
+
+
+ ), +]; + +Object.defineProperty(NextImage, 'default', { + configurable: true, + value: props => , +}); + +export default preview; diff --git a/.storybook/test-runner.ts b/.storybook/test-runner.ts new file mode 100644 index 0000000000000..81da6dcd41940 --- /dev/null +++ b/.storybook/test-runner.ts @@ -0,0 +1,15 @@ +import type { TestRunnerConfig } from '@storybook/test-runner'; + +const config: TestRunnerConfig = { + postRender: async (page, context) => { + // Gather the page HTML inner content for a DOM HTML Snapshot + const rootElementId = '[data-test-id="story-root"]'; + const rootElement = await page.locator(rootElementId); + const content = await rootElement.innerHTML(); + + expect(content).toBeDefined(); + expect(content.replace(/class="(.*?)"/gm, '')).toMatchSnapshot(); + }, +}; + +export default config; diff --git a/.stylelintignore b/.stylelintignore index 5021b0283305d..6899f056569a8 100644 --- a/.stylelintignore +++ b/.stylelintignore @@ -1,2 +1,14 @@ -/build/ -**/vendor/ +# Next.js files +.next +.turbo +.swc +build + +# Public Folder +public + +# Jest +coverage + +# Storybook +storybook-static diff --git a/.stylelintrc b/.stylelintrc index 7d578a7d42f52..4d5d667a4b9fe 100644 --- a/.stylelintrc +++ b/.stylelintrc @@ -1,12 +1,14 @@ { - "extends": [ - "stylelint-config-twbs-bootstrap" - ], + "extends": ["stylelint-config-recommended-scss"], + "plugins": ["stylelint-order", "stylelint-selector-bem-pattern"], "rules": { - "declaration-no-important": null, - "order/properties-order": null, - "selector-max-id": 1, - "selector-max-type": null, - "selector-no-qualifying-type": null + "order/properties-alphabetical-order": true, + "no-descending-specificity": null, + "scss/at-extend-no-missing-placeholder": null, + "scss/at-import-no-partial-leading-underscore": null, + "selector-pseudo-class-no-unknown": [ + true, + { "ignorePseudoClasses": ["global"] } + ] } } diff --git a/CODEOWNERS b/CODEOWNERS index 4463b482983fd..6ef71ee32479d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1,18 +1,8 @@ -# Localization teams -/locale/ar/ @nodejs/nodejs-ar -/locale/ca/ # No Catalan team -/locale/de/ @nodejs/nodejs-de -/locale/es/ @nodejs/nodejs-es -/locale/fa/ @nodejs/nodejs-fa -/locale/fr/ @nodejs/nodejs-fr -/locale/gl/ # No Galacian team -/locale/it/ @nodejs/nodejs-it -/locale/ja/ @nodejs/nodejs-ja -/locale/ko/ @nodejs/nodejs-ko -/locale/pt-br/ @nodejs/nodejs-pt -/locale/ro/ @nodejs/nodejs-ro -/locale/ru/ @nodejs/nodejs-ru -/locale/tr/ @nodejs/nodejs-tr -/locale/uk/ @nodejs/nodejs-uk -/locale/zh-cn/ @nodejs/nodejs-cn -/locale/zh-tw/ @nodejs/nodejs-tw +# Default rules + +* @nodejs/website + +# Node.js Release Blog Posts + +/pages/en/blog/release @nodejs/releasers +/pages/en/blog/announcements @nodejs/releasers diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000000000..8fff30e0a6cca --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,4 @@ +# Code of Conduct + +- [Node.js Code of Conduct](https://github.com/nodejs/admin/blob/HEAD/CODE_OF_CONDUCT.md) +- [Node.js Moderation Policy](https://github.com/nodejs/admin/blob/HEAD/Moderation-Policy.md) diff --git a/COLLABORATOR_GUIDE.md b/COLLABORATOR_GUIDE.md index a88b692b9c26c..dbf94ad62a154 100644 --- a/COLLABORATOR_GUIDE.md +++ b/COLLABORATOR_GUIDE.md @@ -1,16 +1,19 @@ # Node.js Collaborator Guide -* [Issues and Pull Requests](#issues-and-pull-requests) -* [Accepting Modifications](#accepting-modifications) - * [Involving the Website Group](#involving-the-website-group) -* [Developer's Certificate of Origin 1.1](#developers-certificate-of-origin-11) -* [Code of Conduct](#code-of-conduct) -* [Code editing](#code-editing) - * [Adding new pages](#adding-new-pages) - * [Create the page content](#create-the-page-content) - * [Update locale site.json to add link attributes](#update-locale-sitejson-to-add-link-attributes) - * [Update the layout to add a link](#update-the-layout-to-add-a-link) - * [Translating pages](#translating-pages) +- [Issues and Pull Requests](#issues-and-pull-requests) +- [Accepting Modifications](#accepting-modifications) + - [Involving the Website Team](#involving-the-website-team) +- [Code editing](#code-editing) + - [Adding new pages](#adding-new-pages) + - [Create the page content](#create-the-page-content) + - [Translating pages](#translating-pages) +- [Creating Components](#creating-components) + - [Best practices when creating a Component](#best-practices-when-creating-a-component) + - [How a new Component should look like when freshly created](#how-a-new-component-should-look-like-when-freshly-created) + - [Best practices for Component development in general](#best-practices-for-component-development-in-general) +- [Unit Tests and Storybooks](#unit-tests-and-storybooks) + - [General Guidelines for Unit Tests](#general-guidelines-for-unit-tests) + - [General Guidelines for Storybooks](#general-guidelines-for-storybooks) This document contains information for Collaborators of the Node.js website project regarding maintaining the code, documentation and issues. @@ -70,100 +73,36 @@ lack of consensus may indicate the need to elevate discussion to the Website Group for resolution (see below). All bugfixes require a test case which demonstrates the defect. The -test should *fail* before the change, and *pass* after the change. +test should _fail_ before the change, and _pass_ after the change. All pull requests that modify executable code should be subjected to continuous integration tests on the [project CI server](https://ci.nodejs.org/). -### Involving the Website Group +### Involving the Website Team Collaborators may opt to elevate pull requests or issues to the group for discussion by mentioning `@nodejs/website`. This should be done where a pull request: -* has a significant impact on the codebase, -* is inherently controversial; or -* has failed to reach consensus amongst the Collaborators who are +- has a significant impact on the codebase, +- is inherently controversial; or +- has failed to reach consensus amongst the Collaborators who are actively participating in the discussion. The Website group should serve as the final arbiter where required. -## Developer's Certificate of Origin 1.1 - -By making a contribution to this project, I certify that: - -* (a) The contribution was created in whole or in part by me and I - have the right to submit it under the open source license - indicated in the file; or - -* (b) The contribution is based upon previous work that, to the best - of my knowledge, is covered under an appropriate open source - license and I have the right under that license to submit that - work with modifications, whether created in whole or in part - by me, under the same open source license (unless I am - permitted to submit under a different license), as indicated - in the file; or - -* (c) The contribution was provided directly to me by some other - person who certified (a), (b) or (c) and I have not modified - it. - -* (d) I understand and agree that this project and the contribution - are public and that a record of the contribution (including all - personal information I submit with it, including my sign-off) is - maintained indefinitely and may be redistributed consistent with - this project or the open source license(s) involved. - -## Code of Conduct - -This Code of Conduct is adapted from [Rust's wonderful -CoC](https://github.com/rust-lang/rust/wiki/Note-development-policy#conduct). - -* We are committed to providing a friendly, safe and welcoming - environment for all, regardless of gender, sexual orientation, - disability, ethnicity, religion, or similar personal characteristic. -* Please avoid using overtly sexual nicknames or other nicknames that - might detract from a friendly, safe and welcoming environment for - all. -* Please be kind and courteous. There's no need to be mean or rude. -* Respect that people have differences of opinion and that every - design or implementation choice carries a trade-off and numerous - costs. There is seldom a right answer. -* Please keep unstructured critique to a minimum. If you have solid - ideas you want to experiment with, make a fork and see how it works. -* We will exclude you from interaction if you insult, demean or harass - anyone. That is not welcome behavior. We interpret the term - "harassment" as including the definition in the [Citizen Code of - Conduct](http://citizencodeofconduct.org/); if you have any lack of - clarity about what might be included in that concept, please read - their definition. In particular, we don't tolerate behavior that - excludes people in socially marginalized groups. -* Private harassment is also unacceptable. No matter who you are, if - you feel you have been or are being harassed or made uncomfortable - by a community member, please contact one of the channel ops or any - of the TC members immediately with a capture (log, photo, email) of - the harassment if possible. Whether you're a regular contributor or - a newcomer, we care about making this community a safe place for you - and we've got your back. -* Likewise any spamming, trolling, flaming, baiting or other - attention-stealing behavior is not welcome. -* Avoid the use of personal pronouns in code comments or - documentation. There is no need to address persons when explaining - code (e.g. "When the developer") - ## Code editing ### Adding new pages 1. Create new page content including the layout, title and copy. -2. Update `/locale/en/site.json` to provide page link attributes. -3. Update the relevant `/layout` to add a link to the new page. +2. Update the relevant `/layout` to add a link to the new page. #### Create the page content -Create a new markdown file in `/local/en`. As specified in the -[README.md](./README.md#layout), initial development happens in English. +Create a new markdown file in `/pages/en`. As specified in the +[README.md](./README.md#structure-of-this-repository), initial development happens in English. At the top of the markdown file, set a page the title and layout. @@ -176,27 +115,124 @@ layout: contribute.hbs [Event copy goes here] ``` -#### Update locale site.json to add link attributes +### Translating pages -Open `local/en/site.json` and find the appropriate page structure. -Add a new object defining the link attributes. +See [TRANSLATION.md](./TRANSLATION.md) for the website translation policy. -```json -"event": { - "link": "get-involved/events", - "text": "Events" -} -``` +## Creating Components + +The Node.js Website uses **React.js** as a Frontend Library for the development of the Website. React allows us to create user interfaces with a modern take on Web Development. + +If you're unfamiliar with React or Web Development in general, we encourage a read before taking on complex issues and tasks as this repository is **not for educational purposes** and we expect you to have a basic understanding of the technologies used. + +We also recommend getting familiar with technologies such as [Next.js][], [MDX][], [SCSS][] and "concepts" such as "CSS Modules" and "CSS-in-JS". + +### Best practices when creating a Component -#### Update the layout to add a link +- All React Components should be placed within the `components` folder. +- Each Component should be placed whenever possible within a sub-folder, which we call the "Domain" of the Component + - The domain is the representation of where these Components belong to or where will be used. + - For example, Components used within Article Pages or that are part of the structure of an Article or the Article Layouts, should be placed within `components/Article` +- Each component should have its own folder with the name of the Component +- The structure of each component folder follows the following template: + ```text + - ComponentName + - index.tsx // the component itself + - index.module.scss // all styles of the component are placed there + - index.stories.tsx // component Storybook stories + - __tests__ // component tests (such as unit tests, etc) + - index.test.tsx + ``` +- React Hooks belonging to a single Component should be placed within the Component's folder + - If the Hook as a wider usability or can be used by other Components, then it should be placed at the root `hooks` folder. +- If the Component has "sub-components" they should follow the same philosophy as the Component itself. + - For example, if the Component `ComponentName` has a sub-component called `SubComponentName`, then it should be placed within `ComponentName/SubComponentName` -Using the example layout, open `/layouts/contribute.hbs` and add your new -link to the markup. It's essential to update the handlebars paths to site.json. +#### How a new Component should look like when freshly created -```handlebars -{{site.locale}}/{{site.getinvolved.events.link}} +```tsx +import styles from './index.module.scss'; +import type { FC } from 'react'; + +type MyComponentProps = {}; // The types of the Props of your Component + +const MyComponent: FC = ({ prop1, prop2... }) => ( + // Actual code of my Component +); + +export default MyComponent; ``` -### Translating pages +### Best practices for Component development in general -See [TRANSLATION.md](./TRANSLATION.md) for the website translation policy. +- Only spread props `{ ... }` on the definition of the Component (Avoid having a variable named `props`) +- Avoid importing `React`, only import the modules from React that you need +- When importing types use `import type { NameOfImport } from 'module'` +- When defining a Component use the `FC` type from React to define the type of the Component + - When using `children` as a prop, use the `FC>` type instead + - Alterenatively you can define your type as `type MyComponentProps = PropsWithChildren<{ my other props}>` +- Each Props type should be prefixed by the name of the Component +- Components should always be the `default` export of a React Component file +- Avoid using DOM/Web APIs/`document`/`window` API access within a React Component. Use utilities or Hooks when you need a Reactive state +- Avoid making your Component too big. Deconstruct it into smaller Components/Hooks whenever possible + +## Unit Tests and Storybooks + +Each new feature or bug fix should be accompanied by a unit test (when deemed valuable). We use [Jest][] as our test runner and [React Testing Library][] for our React unit tests. + +We also use [Storybook][] to document our components. Each component should have a storybook story that documents the component's usage. Snapshot testing of our components is directly done by taking snapshot of all Storybook stories, using [Storybook Test Runner][] and [Playwright][]. + +### General Guidelines for Unit Tests + +Unit Tests are fundamental to ensure that code changes do not disrupt the functionalities of the Node.js Website: + +- We recommend that unit tests are added for content covering `util`, `scripts`, `hooks` and `components` whenever possible. +- Unit Tests should cover that the functionality of a given change is working as expected. +- When creating unit tests for React components, we recommend that the tests cover all the possible states of the component. +- We also recommend mocking external dependencies, if unsure about how to mock a certain dependency, raise the question on your Pull Request. + - We recommend using [Jest's Mock Functions](https://jestjs.io/docs/en/mock-functions) for mocking dependencies. + - We recommend using [Jest's Mock Modules](https://jestjs.io/docs/en/manual-mocks) for mocking dependencies that are not available on the Node.js runtime. + - Common Providers and Contexts from the lifecycle of our App, such as [`react-intl`][] should not be mocked but given an empty or fake context whenever possible. +- We recommend reading previous unit tests from the codebase for inspiration and code guidelines. + +### General Guidelines for Storybooks + +Storybooks are an essential part of our development process. They help us to document our components and to ensure that the components are working as expected. + +They also allow Developers to preview Components and be able to test them manually/individually to the smallest unit of the Application. (The individual Component itself). + +**Storybooks should be fully typed and follow the following template:** + +```tsx +import type { Meta as MetaObj, StoryObj } from '@storybook/react'; +import NameOfComponent from './index'; + +type Story = StoryObj; +type Meta = MetaObj; + +// If the component has any props that are interactable, they should be passed here +// We recommend reading Storybook docs for args: https://storybook.js.org/docs/react/writing-stories/args +export const Default: Story = {}; + +// If the Component has more than one State/Layout/Variant, there should be one Story for each variant +export const AnotherStory: Story = { + args: {}, +}; + +export default { component: NameOfComponent } as Meta; +``` + +- Stories should have `args` whenever possible, we want to be able to test the different aspects of a Component +- Please follow the template above to keep the Storybooks as consistent as possible +- We recommend reading previous Storybooks from the codebase for inspiration and code guidelines. +- If you need to decorate/wrap your Component/Story with a Container/Provider, please use [Storybook Decorators](https://storybook.js.org/docs/react/writing-stories/decorators) + +[Jest]: https://jestjs.io/ +[React Testing Library]: https://testing-library.com/docs/react-testing-library/intro/ +[Storybook]: https://storybook.js.org/ +[Storybook Test Runner]: https://storybook.js.org/addons/@storybook/test-runner#dom-snapshot-recipe +[Playwright]: https://playwright.dev/ +[`react-intl`]: https://formatjs.io/docs/react-intl/ +[Next.js]: https://nextjs.org/ +[MDX]: https://mdxjs.com/ +[SCSS]: https://sass-lang.com/ diff --git a/CONTENT_VS_CODE.md b/CONTENT_VS_CODE.md new file mode 100644 index 0000000000000..43342c46db78b --- /dev/null +++ b/CONTENT_VS_CODE.md @@ -0,0 +1,7 @@ +## Content vs. Code + +The **Website Team** (@nodejs/website) is primarily concerned with the code and overall structure of the website. + +The content of the website comes from a variety of working groups [(Release WG, Ecosystem Security WG and others)](https://github.com/nodejs/TSC/blob/main/WORKING_GROUPS.md#current-working-groups). + +The Website team defers to these WGs on matters of content and routinely adds collaborators from these working groups as they add and improve content on the website. In other words, the Website team is not an **editorial** team except when no other Working Group has taken responsibility for a content area, meaning we are the default editors for that content. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6c1fa597134e0..a1a33198b2201 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,62 +1,226 @@ -# Node.js Community Contributing Guide 1.0 +# Node.js Website Contributing Guide -## Code of Conduct +Thank you for your interest in contributing to the Node.js Website. Before you proceed, briefly go through the following: -The Code of Conduct explains the _bare minimum_ behavior -expectations the Node Foundation requires of its contributors. -[Please read it before participating](https://github.com/nodejs/admin/blob/HEAD/CODE_OF_CONDUCT.md). +- [Code of Conduct](https://github.com/nodejs/node/blob/HEAD/CODE_OF_CONDUCT.md) +- [Contributing](#contributing) +- [Getting started](#getting-started) + - [CLI Commands](#cli-commands) +- [Commit Guidelines](#commit-guidelines) +- [Pull Request Policy](#pull-request-policy) +- [Becoming a collaborator](#becoming-a-collaborator) +- [Developer's Certificate of Origin 1.1](#developers-certificate-of-origin-11) -## Vocabulary +# Contributing -* A **Contributor** is any individual creating or commenting on an issue or pull request. -* A **Collaborator** is a subset of contributors who have been given write access to the repository. +Any individual is welcome to contribute to the Node.js Website. The repository currently has two kinds of contribution personas: -## Logging Issues +- A **Contributor** is any individual who creates an issue/PR, comments on an issue/PR + or contributes in some other way. +- A **Collaborator** is a contributor with write access to the repository. See [here](#becoming-a-collaborator) on how to become a collaborator. -Log an issue for any question or problem you might have. When in doubt, log an issue. -Any additional policies about what to include will be provided in the responses. The only -exception is security disclosures which should be sent privately. +You can find more details and guides about Collaborating with this repository through our [Collaborator Guide](./COLLABORATOR_GUIDE.md). -Collaborators may direct you to another repository, ask for additional clarifications, and -add appropriate metadata before the issue is addressed. +## Becoming a Collaborator -Please be courteous, respectful, and every participant is expected to follow the -project's Code of Conduct. +A collaborator of the Node.js Website repository is a member of the Node.js Website Team. -## Contributions +The Website Team is responsible for the technical development of the Node.js Website, thus it is expected +that team members have significant knowledge about modern Web Technologies and Web Standards. -Any change to resources in this repository must be through pull requests. +Note that regular contributors do not need to become "Collaborators". Any contribution is appreciated and a Collaborator status +is a formality that comes with obligations. -No pull request can be merged without being reviewed. +If you're an active contributor seeking to become a member we recommend reaching out to one of the existing Team Members for guidance. -For non-trivial contributions, pull requests should sit for at least 36 hours to ensure that -contributors in other timezones have time to review. Consideration should also be given to -weekends and other holiday periods to ensure active collaborators all have reasonable time to -become involved in the discussion and review process if they wish. +
+ What's the process for becoming a Collaborator? -The default for each contribution is that it is accepted once no collaborator has an objection. -During review collaborators may also request that a specific contributor who is most versed in a -particular area gives a "LGTM" before the PR can be merged. There is no additional "sign off" -process for contributions to land. Once all issues brought by collaborators are addressed it can -be landed by any collaborator. +- You must be actively contributing to this repository. +- Contributions must include significant code reviews or code contributions. +- A nomination must be done by an existing Team Member of the Website Team with an Issue + - The Issue must explain and describe why the nominated person is a good addition to the team + - The Issue must contain links to relevant contributions through: + - Code Reviews + - Comments on Issues and PRs + - Authoring of PRs or Issues + - Comments or Authoring of Discussions +- The nomination must have at least 3 existing members of the Website Team to be in agreement with the nomination. + - This can be done through commenting with "agreement" (showing support) or reacting to the Issue with a :+1: (Thumbs-up Emoji) +- The Issue must be open for at least 72 hours without an objection from an existing member of the Website Team - The nomination cannot pass until all open discordances/objections are resolved. - Objections coming from the TSC or Core Collaborators are also counted as valid objections. +
-In the case of an objection being raised in a pull request by another collaborator, all involved -collaborators should seek to arrive at a consensus by way of addressing concerns being expressed -by discussion, compromise on the proposed change, or withdrawal of the proposed change. +# Getting started -If a contribution is controversial and collaborators cannot agree about how to -get it landed or if it should be landed, then it should be escalated to the -[Node.js Technical Steering Committee](https://github.com/nodejs/tsc). Only a -small minority of issues are brought to the Technical Steering Committee for -resolution. Discussion and compromise among collaborators is the default -resolution mechanism. +The steps below will give you a general idea of how to prepare your local environment for the Node.js Website and general steps +for getting things done and landing your contribution. -## Becoming a Collaborator +1. Click the fork button in the top right to clone the [nodejs.org repository](https://github.com/nodejs/nodejs.org/fork) +2. Clone your fork using SSH, GitHub CLI, or HTTPS. + + ```bash + git clone git@github.com:/nodejs.org.git # SSH + git clone https://github.com//nodejs.org.git # HTTPS + gh repo clone /nodejs.org # GitHub CLI + ``` + +3. Change into the nodejs.org directory. + + ```bash + cd nodejs.org + ``` + +4. Create a remote for keeping your fork as well as your local clone up-to-date. + + ```bash + git remote add upstream git@github.com:nodejs/nodejs.org.git # SSH + git remote add upstream https://github.com/nodejs/nodejs.org.git # HTTPS + gh repo sync nodejs/nodejs.org # GitHub CLI + ``` + +5. Create a new branch for your work. + + ```bash + git checkout -b name-of-your-branch + ``` + +6. Run the following to install the dependencies and start a local preview of your work. + + ```bash + npm ci # installs this project's dependencies + npx turbo serve # starts a preview of your local changes + ``` + +7. Perform a merge to sync your current branch with the upstream branch. + + ```bash + git fetch upstream + git merge upstream/main + ``` + +8. Run `npx turbo format` to confirm that linting, and formatting are passing. + + ```bash + npx turbo format + ``` + +9. Once you're happy with your changes, add and commit them to your branch, then push the branch to your fork. + + ```bash + cd ~/nodejs.org + git add . + git commit -m "some message" + git push -u origin name-of-your-branch + ``` + +10. Create a Pull Request. + +> **Note**: Before committing and opening a Pull Request please go first through our [Commit](#commit-guidelines) and [Pull Request](#pull-request-policy) guidelines outlined below. + +## CLI Commands + +This repository contains several scripts and commands for performing numerous tasks. The most relevant ones are described below. + +
+ Commands for Running & Building the Website + +- `npx turbo serve` runs Next.js's Local Development Server, listening by default on `http://localhost:3000/`. +- `npx turbo build` builds the Application on Production mode. The output is by default within `.next` folder. + - This is used for the Node.js Vercel Deployments (Preview & Production) +- `npx turbo deploy` builds the Application on Export Production Mode. The output is by default within `build` folder. + - This is used for the Node.js Legacy Website Server (DigitalOcean) +- `npx turbo start` starts a web server running serving the built content from `npx turbo build` +
+ +
+ Commands for Maintenance Tasks and Tests + + - `npx turbo lint` runs the linter for all the js files. + - `npx turbo lint:fix` attempts to fix any linting errors + - `npx turbo prettier` runs the prettier for all the js files. + - `npx turbo prettier:fix` attempts to fix any style errors + - `npx turbo format` formats and fixes the whole codebase + - `npx turbo scripts:release-post` generates a release post for the current release + - **Usage:** `npx turbo scripts:release-post -- --version=vXX.X.X --force` + - `npx turbo storybook` starts Storybook's local server + - `npx turbo storybook:build` builds Storybook as a static web application for publishing + - `npx turbo test` runs all tests locally + - `npx turbo test:unit` runs jest (unit-tests) locally + - `npx turbo test:storybook` runs storybook test-runner tests + - `npx turbo test:storybook:snapshot` generates and updates snapshots for all storybook components. +
+ +# Commit Guidelines + +This project follows the [Conventional Commits][] specification. + +Commits should be signed. You can read more about [Commit Signing][] here. + +### Commit Message Guidelines + +- Commit messages must include a "type" as described on Conventional Commits +- Commit messages **must** start with a capital letter +- Commit messages **must not** end with a period `.` + +### Pre-commit Hooks + +This project uses [husky][] for pre-commit hooks. + +Some JSON files are generated during Build time with empty files as placeholders. Build time happens when you run `npx turbo serve` or `npx turbo build`. We don't want to commit those unnecessary changes. Since these files exist in the repository, `.gitignore` won't work for them. As the workaround, we have a pre-commit hook to discard those changes. + +# Pull Request Policy + +This policy governs how contributions should land within this repository. The lines below state the checks and policies to be followed before merging and on the act of merging. + +## Before merging + +We recommend a read on our [Collaborator Guide](COLLABORATOR_GUIDE.md#accepting-modifications) for in-depth details on how we accept contributions into this repository. The list below describes some of the merging and approval rules adopted in this repository. + +- Pull Requests must be open for at least 48 hours (Or 72 hours if the PR was authored on the weekend). + - Pull requests might be immediately merged if they contain critical bug fixes, short errata (e.g. typos from previous PRs) or any critical change that is considered a "showstopper" for the operation of the website. + - This kind of PRs should only be done by existing collaborators that have write-access and/or signed off by administrators/maintainers. + - This rule cannot be used for updates on the `COLLABORATOR_GUIDE.md`, `CONTRIBUTING.md` guide, `CODEOWNERS`, GitHub Actions or any security-impacting file or document that changes the governing policies of this repository. + - Pull requests might be "fast-tracked", which means, they can be merged before the usual 48 hours' notice if a "fast-track" label is added. + - The person that is fast-tracking the PR (adding the label) must also comment on the PR that they're requesting the PR to be fast-tracked + - The comment must mention `@nodejs/website` and must have at least one 👍 (or any other sort of approval reaction) if the person fast-tracking the PR is the author of the PR. + - Fast-tracking is only allowed for small bug fixes, small feature changes, localisation changes, or other sorts of non-critical/highly-impacting changes that are not covered by the previous rule that allows PRs to be merged immediately. + - Fast-tracking cannot be used for updates on the `COLLABORATOR_GUIDE.md`, `CONTRIBUTING.md` guide, `CODEOWNERS`, GitHub Actions, or any security-impacting file or document that changes the governing policies of this repository. +- There must be no objections after a 48-hour period (Or 72 hours if the PR was authored on the weekend). +- At least one approval is required for any PR to be merged. +- Tests must be included in Pull Requests for new features or bug fixes. If any test(s) are failing, you are responsible for fixing them. + +Each contribution is accepted only if there is no objection to it by a collaborator. During the review, collaborators may request that a specific contributor who is an expert in a particular area give an "LGTM" before the PR can be merged. + +In the case that an objection is raised in a pull request by another collaborator, all collaborators involved should try to arrive at a consensus by addressing the concerns through discussion, compromise, or withdrawal of the proposed change(s). + +## When merging + +- All required Status-checks must have passed. +- All discussions must be resolved. +- [`squash`][] pull requests made up of multiple commits + +# Developer's Certificate of Origin 1.1 -Individuals with non-trivial contributions may be added as Collaborators. -Collaborators are expected to follow this policy and continue to send pull -requests and go through proper review. +``` +By contributing to this project, I certify that: -## How to contribute +- (a) The contribution was created in whole or in part by me and I have the right to + submit it under the open source license indicated in the file; or +- (b) The contribution is based upon previous work that, to the best of my knowledge, + is covered under an appropriate open source license and I have the right under that + license to submit that work with modifications, whether created in whole or in part + by me, under the same open source license (unless I am permitted to submit under a + different license), as indicated in the file; or +- (c) The contribution was provided directly to me by some other person who certified + (a), (b) or (c) and I have not modified it. +- (d) I understand and agree that this project and the contribution are public and that + a record of the contribution (including all personal information I submit with it, + including my sign-off) is maintained indefinitely and may be redistributed consistent + with this project or the open source license(s) involved. -Please read more at [How to contribute](/README.md#contributing) +``` + +[`squash`]: https://help.github.com/en/articles/about-pull-request-merges#squash-and-merge-your-pull-request-commits +[Conventional Commits]: https://www.conventionalcommits.org/ +[Commit Signing]: https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits +[husky]: https://typicode.github.io/husky/ diff --git a/DEPENDENCY_PINNING.md b/DEPENDENCY_PINNING.md new file mode 100644 index 0000000000000..617b09148b243 --- /dev/null +++ b/DEPENDENCY_PINNING.md @@ -0,0 +1,35 @@ +## Dependency Pinning + +Based on the initial discussions from [this discussion thread](https://github.com/nodejs/nodejs.org/discussions/5491), we've decided to use a more strict strategy for handling NPM dependencies within the Node.js Website. + +The intent here is to prevent the build process, or the website itself, from breaking due to changes in dependencies. As some dependencies do not respect semantic versioning, this is a real concern. Pinning dependencies also ensures that we stay fixed on a specific dependency version. For security updates, Dependabot is still configured to give us security alerts when specific dependencies got security advisories. + +### When adding dependencies + +The following recommendations are in order when adding a new dependency: + +- A dependency should be a `dependencies` if it is part of the build process of the Website or used within runtime code. + - Some non-code dependencies are required for the bootstrap of the repository. They are either used on basic build scripts or Git Hooks. Examples include `husky`, `lint-staged` and others. +- A dependency should be a `devDependencies` if it is not invoked anywhere within the codebase. + - This applies to runtimes, tooling, utility/dev-only commands, type packages and others +- Sometimes, a dependency should be a `devDependency` even if invoked within the codebase, but only used within a development environment or during a test runner. Examples include `jest`, `storybook` and others. +- A dependency should be a `peerDependencies` if it is a runtime dependency of the Website, but the Website itself does not install it. Examples include `react`, `react-dom` and others. + +### When pinning dependencies + +When adding dependencies, you should consider if that dependency should be saved as an exact dependency (`--save-exact`) or use either a `^` or `~` version range operator. The following guidelines are in order: + +- A dependency, in general, should be pinned to its exact dependency if it's either a tooling or a CLI dependency. Examples include `husky`, `prettier`, `jest` and others. +- A dependency should generally use `~` if we're interested in patch updates (such as hot-fixes and bug-fixes) and the package is part of the Development or Testing Environment. (Such as `storybook`, for example) +- A dependency should generally use `^` if they're part of the Website Application itself, such as `react`, `react-intl` etc. This is done because we intentionally want to get these dependencies' latest features and bug-fixes. + - If we're not interested in getting the latest features and bug fixes, we should consider using `~` instead. +- Node. js-only dependencies used in scripts or during the build process of the Website (not used within actual Application code) should use `~` instead. Examples include `glob`, `@nodevu/core` +- TypeScript type packages of corresponding packages should follow the same `semver` of their respective packages + +### About manual updates + +This document intends to outline the strategy we have when adding dependencies. We also recommend that Team members only add new dependencies when explicitly needed. The more dependencies, the harder it gets to review and understand the complexity of the Website Application. You can avoid adding new dependencies if possible. + +Manual updates should be avoided. Dependabot is configured for updating our dependencies. Updating a dependency is only needed if the update breaks the current `semver` constraint. In these situations, good judgement should be used (as a dependency should generally only be updated in these scenarios if the updated version brings new features desired by the team). + +Lastly, in general, a Pull Request should not contain `package-lock.json` changes, nor changes to dependencies on `package.json`. diff --git a/README.md b/README.md index de55448c67f50..82afde8ebb259 100644 --- a/README.md +++ b/README.md @@ -1,95 +1,104 @@ -# [nodejs.org](https://nodejs.org/) - -[![CI Status](https://github.com/nodejs/nodejs.org/actions/workflows/ci.yml/badge.svg)](https://github.com/nodejs/nodejs.org/actions/workflows/ci.yml?query=branch%3Amain) -[![MIT Licensed](https://img.shields.io/badge/license-MIT-blue)](LICENSE) -[![Crowdin](https://badges.crowdin.net/nodejs-website/localized.svg)](https://crowdin.com/project/nodejs-website) +

+
+ + + + + + +

+ +

+ Node.js Website built using Next.js with TypeScript, SCSS and MDXv2 +

+ +

+ + MIT License + + + Crowdin Badge + + + + + Powered by Vercel + + + +
+ + Build and Analysis Checks + +
+
+

## What is this repo? -[nodejs.org](https://nodejs.org/) by the [OpenJS Foundation](https://openjsf.org/) builds on the merged community's past website projects to form a self-publishing, community-managed version of the previous site. - -On a technical level, inspiration has been taken from the `iojs.org` repo while design and content has been migrated from the old [nodejs.org repo](https://github.com/nodejs/nodejs.org-archive). These technical changes have helped to facilitate community involvement and empower the foundation's internationalization communities to provide alternative website content in other languages. - -This repo's issues section has become the primary home for the Website WG's coordination efforts (meeting planning, minute approval, etc). - -## Contributing +[Nodejs.org](https://nodejs.org/) by the [OpenJS Foundation](https://openjsf.org/) is the official website for the Node.js® JavaScript runtime. This repo is the source code for the website. It is built using [Next.js](https://nextjs.org), a React Framework. -There are two ways to contribute to this project. The first is **submitting new features or fixing bugs** and the second is **translating content to other languages**. +### Quick-Start Locally -In both cases the workflow is different, please check how it is done in each case. +```bash +npm ci +npx turbo serve -### To submit a new feature or a bugfix +# listening at localhost:3000 +``` -Please contribute! There are plenty of [good first issues](https://github.com/nodejs/nodejs.org/labels/good%20first%20issue) to work on. To get started, you have to [fork](https://github.com/nodejs/nodejs.org/fork) this repo to your own GitHub account first. Then open up a terminal on your machine and enter the following commands: +### Structure of this Repository + +- Page templates are in `/layouts` +- Global styles are in `/styles` +- Public files are in `/public` +- i18n configuration is on `i18n/config.json` +- Global static files are in `/public/static` + - Legacy static files are in `/public/static/legacy` +- All content is in `/pages` + - Initial development usually happens in English: `/pages/en` + - `/i18n/locales/{{locale}}.json` is where global localization information lives. + - All content is in Markdown and is per locale. + - The top of each Markdown file is a block of YAML (Frontmatter) for page-specific localization information passed to various templates. + - The bulk of the Markdown content for each page is referenced as `{children}` on their respective JSX Layout (`layouts/`) -```bash -git clone https://github.com//nodejs.org.git -cd nodejs.org -npm install -npm start -``` +## Contributing -This will start the development server on `http://localhost:8080/en/`. This page should reload automatically when you make changes to the code, but no code is perfect, so sometimes you may need to restart it. :) +This project adopts the Node.js [Code of Conduct][]. -If you want to submit a new feature or a bugfix, the best way is to create the changes in a separate branch, e.g.: `git checkout -b feature/mycoolfeature`. This will make it easier for you to submit a pull request and get your feature merged. +Any person who wants to contribute to the Website is welcome! Please read [Contribution Guidelines][] and see the [Figma Design][] to understand better the structure of this repository. ### To translate content into other languages -If you want to help translate to other languages or improve existing translations, it isn't necessary to work from GitHub. You can and should do it through Crowdin, this is the correct workflow. +If you want to help translate to other languages or improve existing translations, it is optional to work from GitHub. You can and should do it through Crowdin; this is the correct workflow. -Crowdin is an online tool that facilitates the user experience for the translator, here is more information: +Crowdin is an online tool that facilitates the user experience for the translator; here is more information: Website translations are handled via [Crowdin](https://crowdin.com/project/nodejs-website). -To help with localization, please read the [TRANSLATION](TRANSLATION.md) guide. - -## Layout - -* Page templates are in `/layouts` -* Global styles are in `/layouts/css` -* Global static files are in `/static` -* All content is in `/locale` - * Initial development usually happens in English: `/locale/en` - * `/locale/{{locale}}/site.json` is where global localization information lives. - * All content is in Markdown and is per locale. - * The top of each Markdown file is a block of YAML for page specific localization information that is passed to various templates. - * The bulk of the Markdown content for each page is referenced as `{{{content}}}` in the corresponding template. - -## Serve/Build Options - -* `DEFAULT_LOCALE={{locale}} node build.js` builds all the translated files present in the locale folder (will display 404 status code if file is not present), the static/css folder for all the Sass files, as well as copy the rest of the static assets to their subfolder in the build directory. -* `DEFAULT_LOCALE={{locale}} node build.js --preserveLocale` the same as `node build.js` but it will add the pages present in the English locale that are missing instead of throwing 404 status code. -* `DEFAULT_LOCALE={{locale}} npm run serve` builds only the files present in the specified locale folder (will display 404 status code if file is not present), then start the default website (`http://localhost:${port}/${mainLocale}`). Here `{port}` is 8080, `{mainLocale}` is `en` or the first specified language. -* `DEFAULT_LOCALE={{locale}} npm run serve -- --preserveLocale` the same as `npm run serve ` but it will add the pages present in the English locale that are missing. -* `npm run serve` builds all the current languages and returns 404 when a file is not present in the current locale, then start the default website (`http://localhost:${port}/${mainLocale}`). Here `{port}` is 8080, `{mainLocale}` is `en` in default. -* `npm run serve -- --preserveLocale` the same as `npm run serve` but it will add the pages present in the English locale that are missing instead of throwing 404 status code. - -## Test Options +To help with localization, please read the [Translation](TRANSLATION.md) guide. -Before submitting, you must pass all the unit tests and syntax checks by running the two commands below: +### Deployment -* `npm-run-all test:lint test:unit` run all the unit test cases in `tests` folder, as well as check syntax with eslint. -* `npm-run-all --parallel test:lint:*` run all the syntax checks for `js`, `md` and other related files. +Full setup is in minus secrets and certificates. The webhook is set up on GitHub for this project and talks to a small Node server on the host, which does the work. See the [github-webhook](https://github.com/rvagg/github-webhook) package for this. -There're also two syntax check commands for you: -* `npm run test:lint:js -- --fix` try to automatically fix some formations for all the js files. -* `npm run test:lint:stylelint -- --fix` try to automatically fix some formations for all the css/scss files. +## Relevant Links -## Notice +[Code of Conduct][] -* Multiple locales can be built by using comma-separated values in the `DEFAULT_LOCALE` variable: `DEFAULT_LOCALE=en,es,it`. -* For other options, see `package.json`. +[Contribution Guidelines][] -## Deployment +[Collaborator Guide][] -Full setup is in minus secrets and certificates. The webhook is setup on GitHub for this project and talks to a small Node server on the host which does the work. See the [github-webhook](https://github.com/rvagg/github-webhook) package for this. +[Figma Design][] -## Content vs. Code +[Content vs Code][] -The Website Working Group is primarily concerned with the code and overall structure of the website. +[Dependency Pinning][] -The content of the website comes from a variety of working groups (Evangelism, Core, i18n, etc). -The Website WG defers to these WGs on matters of content and routinely adds collaborators from these -working groups as they add and improve content on the website. In other words, the Website WG is not -an *editorial* Working Group except when no other Working Group has taken responsibility for a -content area. +[code of conduct]: https://github.com/nodejs/admin/blob/main/CODE_OF_CONDUCT.md +[contribution guidelines]: ./CONTRIBUTING.md +[content vs code]: ./CONTENT_VS_CODE.md +[dependency pinning]: ./DEPENDENCY_PINNING.md +[collaborator guide]: ./COLLABORATOR_GUIDE.md +[figma design]: https://www.figma.com/file/lOxAGGg5KXb6nwie7zXkz6/NJ---Design-System?node-id=22%3A6086 diff --git a/TRANSLATION.md b/TRANSLATION.md index 7007778270e08..657c489fc7ee0 100644 --- a/TRANSLATION.md +++ b/TRANSLATION.md @@ -1,76 +1,63 @@ # Node.js Website Translation Policy -Node.js is a global platform and so this site has many translations. The translation of the site into -languages other than English is handled by the localization working group of the language in question. If you -would like to contribute to the translation of nodejs.org, please refer to the following process: - -> Since Apr, 1 2020 translation process moved to [Crowdin](https://crowdin.com/project/nodejs-website) - -## Get started - -1. Open [nodejs-website](https://crowdin.com/project/nodejs-website) Crowdin project -2. Find your locale and start translation. Find more details in [guide for volunteer translators](https://support.crowdin.com/for-volunteer-translators/) - -All translated and approved content will be pushed to this repo automatically. You don't need to create any PRs with translation. Just keep localization process on Crowdin. - -Original source can be found in [/locale/en](https://github.com/nodejs/nodejs.org/tree/main/locale/en). If you find any problem with original source, please create a PR with changes directly to `/locale/en`. Crowdin automatically pull all updates within 24 hours. - -### Can't find my locale on Crowdin - -Please create a [new issue](https://github.com/nodejs/nodejs.org/issues/new?template=03-i18n.md) in this repo. Crowdin managers team would be happy to add new languages. - -## Localization groups - -An existing localization group is not required to start translation. You can contribute on Crowdin without it. Think about groups like a basement to communicate with other translators of your locale. - -Contact your appropriate localization group, and discuss with them the best possible way to contribute. A list of the localization groups can be found below. - -* [`nodejs-ar`](https://github.com/nodejs/nodejs-ar) Arabic Community -* [`nodejs-bg`](https://github.com/nodejs/nodejs-bg) Bulgarian Community -* [`nodejs-bn`](https://github.com/nodejs/nodejs-bn) Bengali Community -* [`nodejs-zh-CN`](https://github.com/nodejs/nodejs-zh-CN) Chinese Community -* [`nodejs-cs`](https://github.com/nodejs/nodejs-cs) Czech Community -* [`nodejs-da`](https://github.com/nodejs/nodejs-da) Danish Community -* [`nodejs-de`](https://github.com/nodejs/nodejs-de) German Community -* [`nodejs-el`](https://github.com/nodejs/nodejs-el) Greek Community -* [`nodejs-es`](https://github.com/nodejs/nodejs-es) Spanish Community -* [`nodejs-fa`](https://github.com/nodejs/nodejs-fa) Persian Community -* [`nodejs-fi`](https://github.com/nodejs/nodejs-fi) Finnish Community -* [`nodejs-fr`](https://github.com/nodejs/nodejs-fr) French Community -* [`nodejs-he`](https://github.com/nodejs/nodejs-he) Hebrew Community -* [`nodejs-hi`](https://github.com/nodejs/nodejs-hi) Hindi Community -* [`nodejs-hu`](https://github.com/nodejs/nodejs-hu) Hungarian Community -* [`nodejs-id`](https://github.com/nodejs/nodejs-id) Indonesian Community -* [`nodejs-it`](https://github.com/nodejs/nodejs-it) Italian Community -* [`nodejs-ja`](https://github.com/nodejs/nodejs-ja) Japanese Community -* [`nodejs-ka`](https://github.com/nodejs/nodejs-ka) Georgian Community -* [`nodejs-ko`](https://github.com/nodejs/nodejs-ko) Korean Community -* [`nodejs-mk`](https://github.com/nodejs/nodejs-mk) Macedonian Community -* [`nodejs-ms`](https://github.com/nodejs/nodejs-ms) Malaysian Community -* [`nodejs-nl`](https://github.com/nodejs/nodejs-nl) Dutch Community -* [`nodejs-no`](https://github.com/nodejs/nodejs-no) Norwegian Community -* [`nodejs-pl`](https://github.com/nodejs/nodejs-pl) Polish Community -* [`nodejs-pt`](https://github.com/nodejs/nodejs-pt) Portuguese Community -* [`nodejs-ro`](https://github.com/nodejs/nodejs-ro) Romanian Community -* [`nodejs-ru`](https://github.com/nodejs/nodejs-ru) Russian Community -* [`nodejs-sv`](https://github.com/nodejs/nodejs-sv) Swedish Community -* [`nodejs-ta`](https://github.com/nodejs/nodejs-ta) Tamil Community -* [`nodejs-tr`](https://github.com/nodejs/nodejs-tr) Turkish Community -* [`nodejs-zh-TW`](https://github.com/nodejs/nodejs-zh-TW) Taiwanese Community -* [`nodejs-uk`](https://github.com/nodejs/nodejs-uk) Ukrainian Community -* [`nodejs-vi`](https://github.com/nodejs/nodejs-vi) Vietnamese Community - -### Group for my locale does not exist - -If you can't find group for your locale: - -1. Translate 1000 strings or more on Crowdin for your locale -2. Find at least one more translator for your locale -3. Create a [new issue](https://github.com/nodejs/nodejs.org/issues/new?template=03-i18n.md) in this repo requesting the creation of a group for your locale - -### Group for my locale is archived - -If you find the group for your locale is archived: - -1. Try to contact members of the group by creating a [new issue](https://github.com/nodejs/nodejs.org/issues/new?template=03-i18n.md) in this repo. Include a mention of the group so members get notified of the issue. -2. If there is no response from members in 7 days and if you have already done 1000 strings or more on Crowdin for your locale, open an issue in https://github.com/nodejs/admin requesting the repository be unarchived. +Node.js is a global platform and so this site has many translations. The translation of the site into languages other than English is handled by [Crowdin translators](https://support.crowdin.com/translation-process-overview/). + +## How to translate + +1. Request to join the [Nodejs-Website in Crowdin](https://crowdin.com/project/nodejs-website) +2. [Select the language you want to translate](https://support.crowdin.com/joining-translation-project/#starting-translation) +3. [Start translating](https://support.crowdin.com/online-editor/) + +### Any questions or feedbacks on Translations + +If you have any questions or feedbacks on current translations, you can [start a discussion](https://crowdin.com/project/nodejs-website/discussions) by choosing the "New Topic" and your language from the right dropdown, or a [conversation](https://support.crowdin.com/conversations/) by adding your translators. + +## How to add a new language + +Go on `/i18n/config.json` and add the new language to the `locales` array. + +Fill the language object with the following fields: + +```json +{ + "code": "fr", + "localName": "Français", + "name": "French", + "langDir": "ltr", + "dateFormat": "DD.MM.YYYY", + "hrefLang": "fr", + "enabled": true +} +``` + +| Field Name | Description | Examples | +| ------------ | ------------------------------------------------------------------------------------------------------ | ------------ | +| `code` | The language code. It must be the same as the folder name | `fr` | +| `localName` | The language name in its own language (it's use in language selector) | `Français` | +| `name` | The language name in English | `French` | +| `langDir` | The direction of the language. `ltr` for left to right, `rtl` for right to left | `ltr` | +| `dateFormat` | The date format. It must be a valid [moment.js format](https://momentjs.com/docs/#/displaying/format/) | `DD.MM.YYYY` | +| `hrefLang` | The language code in [ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) format | `fr` | +| `enabled` | If the language is enabled or not | `true` | + +Please also add the new locale file to the locales folder `/i18n/locales` and import it in the `/i18n/locales/index.mjs` file. + +## Adding new Translation Keys + +If you're making a new Component and adding Translation Keys for your Component, they should follow these guidelines: + +- Only add the new translation keys on the `i18n/locales/en.json` file. Crowdin will handle on syncing the files and letting translators know there are new keys to be translated +- The translation keys should have the prefix as the canonical path of your Component. If your Component is `components/Common/MyComponent` the prefix key should be `components.common.myComponent` + - The Translation Key suffix should be easy to understand and semantic. For example, if the key is about "the text of a button that when interacted it copies content to the clipboard", the suffix should probably be `copyButton.title`. The final translation key would be `components.common.myComponent.copyButton.title` + - Translation Keys should be in Camel Case only. + - The values of each Translation Key should follow the [ICU Message Syntax](https://formatjs.io/docs/core-concepts/icu-syntax/) +- All new Translation keys should be added at the bottom of the `i18n/locales/en.json` file. Since this makes it easier for Translators to notice that there are new Translation keys to be translated. + +### Translations and Unit Testing + +Translation Keys should not be translated during Unit Testing. If your Component uses, for example `FormattedMessage`, you should provide the `` surrounding your `testing-library` render logic, or you can create a wrapper for your test. Note that you should not import the English messages to your Unit Test as: + +- Unit Testing should test a Component functionality. +- Unit Tests should not rely on text, titles, or string bags, as these texts will change arbitrarily and make the test suite fail. + - In this case, you should test your component by aria-text, or other `aria-*` attributes or even by class names or other artifacts. +- If you want to test how different languages and text appear within a Component, Visual Regression Testing is recommended. diff --git a/__fixtures__/nodeReleases.tsx b/__fixtures__/nodeReleases.tsx new file mode 100644 index 0000000000000..a4f69beddcf68 --- /dev/null +++ b/__fixtures__/nodeReleases.tsx @@ -0,0 +1,97 @@ +import type { NodeRelease } from '../types'; + +export const createNodeReleases = (): NodeRelease[] => [ + { + currentStart: '2023-04-18', + ltsStart: '2023-10-24', + maintenanceStart: '2024-10-22', + endOfLife: '2026-04-30', + major: 20, + version: '20.2.0', + versionWithPrefix: 'v20.2.0', + codename: '', + isLts: false, + status: 'Current', + npm: '9.6.6', + v8: '11.3.244.8', + releaseDate: '2023-05-16', + modules: '115', + }, + { + currentStart: '2022-10-18', + maintenanceStart: '2023-04-01', + endOfLife: '2023-06-01', + major: 19, + version: '19.9.0', + versionWithPrefix: 'v19.9.0', + codename: '', + isLts: false, + status: 'End-of-life', + npm: '9.6.3', + v8: '10.8.168.25', + releaseDate: '2023-04-10', + modules: '111', + }, + { + currentStart: '2022-04-19', + ltsStart: '2022-10-25', + maintenanceStart: '2023-10-18', + endOfLife: '2025-04-30', + major: 18, + version: '18.16.0', + versionWithPrefix: 'v18.16.0', + codename: 'Hydrogen', + isLts: true, + status: 'Active LTS', + npm: '9.5.1', + v8: '10.2.154.26', + releaseDate: '2023-04-12', + modules: '108', + }, + { + currentStart: '2021-10-19', + maintenanceStart: '2022-04-01', + endOfLife: '2022-06-01', + major: 17, + version: '17.9.1', + versionWithPrefix: 'v17.9.1', + codename: '', + isLts: false, + status: 'End-of-life', + npm: '8.11.0', + v8: '9.6.180.15', + releaseDate: '2022-06-01', + modules: '102', + }, + { + currentStart: '2021-04-20', + ltsStart: '2021-10-26', + maintenanceStart: '2022-10-18', + endOfLife: '2023-09-11', + major: 16, + version: '16.20.0', + versionWithPrefix: 'v16.20.0', + codename: 'Gallium', + isLts: true, + status: 'Maintenance LTS', + npm: '8.19.4', + v8: '9.4.146.26', + releaseDate: '2023-03-28', + modules: '93', + }, + { + currentStart: '2020-10-20', + maintenanceStart: '2021-04-01', + endOfLife: '2021-06-01', + major: 15, + version: '15.14.0', + versionWithPrefix: 'v15.14.0', + codename: '', + isLts: false, + status: 'End-of-life', + npm: '7.7.6', + v8: '8.6.395.17', + releaseDate: '2021-04-06', + modules: '88', + }, +]; diff --git a/app/en/feed/[feed]/route.ts b/app/en/feed/[feed]/route.ts new file mode 100644 index 0000000000000..9f237b3236cd9 --- /dev/null +++ b/app/en/feed/[feed]/route.ts @@ -0,0 +1,33 @@ +import { NextResponse } from 'next/server'; +import * as nextJson from '@/next.json.mjs'; +import * as nextData from '@/next.data.mjs'; + +// loads all the data from the blog-posts-data.json file +const websiteFeeds = nextData.generateWebsiteFeeds(nextJson.blogData); + +type StaticParams = { params: { feed: string } }; + +// This is the Route Handler for the `GET` method which handles the request +// for Blog Feeds within the Node.js Website +// @see https://nextjs.org/docs/app/building-your-application/routing/router-handlers +export const GET = (_: Request, { params }: StaticParams) => { + if (params.feed.includes('.xml') && websiteFeeds.has(params.feed)) { + return new NextResponse(websiteFeeds.get(params.feed)?.rss2(), { + headers: { 'Content-Type': 'application/xml' }, + }); + } + + return new NextResponse(null, { status: 404 }); +}; + +// This function generates the static paths that come from the dynamic segments +// `en/feeds/[feed]` and returns an array of all available static paths +// this is useful for static exports, for example. +// Note that differently from the App Router these don't get built at the build time +// only if the export is already set for static export +export const generateStaticParams = () => + [...websiteFeeds.keys()].map(feed => ({ feed })); + +// Enforces that this route is used as static rendering +// @see https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamic +export const dynamic = 'error'; diff --git a/app/sitemap.xml/route.ts b/app/sitemap.xml/route.ts new file mode 100644 index 0000000000000..5f1e6cb95ed88 --- /dev/null +++ b/app/sitemap.xml/route.ts @@ -0,0 +1,34 @@ +import { getServerSideSitemap } from 'next-sitemap'; +import * as nextDynamic from '@/next.dynamic.mjs'; +import * as nextConstants from '@/next.constants.mjs'; + +// This is the combination of the Application Base URL and Base PATH +const canonicalUrl = `${nextConstants.BASE_URL}${nextConstants.BASE_PATH}`; + +// This method populates and generates the Website Sitemap by using `next-sitemap` SSR functionality +// @see https://nextjs.org/docs/app/building-your-application/routing/router-handlers +export const GET = () => { + // Retrieves all the dynamic generated paths + const dynamicRoutes = nextConstants.DYNAMIC_GENERATED_ROUTES(); + + // Retrieves all the static paths (from next.dynamic.mjs) + const staticPaths = [...nextDynamic.allPaths.values()] + .flat() + .filter(route => nextConstants.STATIC_ROUTES_IGNORES.every(e => !e(route))) + .map(route => route.routeWithLocale); + + // The current date of this request + const currentDate = new Date().toISOString(); + + return getServerSideSitemap( + [...dynamicRoutes, ...staticPaths].sort().map(route => ({ + loc: `${canonicalUrl}/${route}`, + lastmod: currentDate, + changefreq: 'always', + })) + ); +}; + +// Enforces that this route is used as static rendering +// @see https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamic +export const dynamic = 'error'; diff --git a/build.js b/build.js deleted file mode 100755 index eba639e7beb69..0000000000000 --- a/build.js +++ /dev/null @@ -1,350 +0,0 @@ -#! /usr/bin/env node - -'use strict'; - -// BUILD.JS: This file is responsible for building static HTML pages - -const fs = require('fs'); -const gracefulFs = require('graceful-fs'); - -// This is needed at least on Windows to prevent the `EMFILE: too many open files` error -// https://github.com/isaacs/node-graceful-fs#global-patching -gracefulFs.gracefulify(fs); - -const path = require('path'); -const Metalsmith = require('metalsmith'); -const collections = require('metalsmith-collections'); -const feed = require('metalsmith-feed'); -const layouts = require('metalsmith-layouts'); -const markdown = require('@metalsmith/markdown'); -const permalinks = require('@metalsmith/permalinks'); -const pagination = require('metalsmith-yearly-pagination'); -const defaultsDeep = require('lodash.defaultsdeep'); -const autoprefixer = require('autoprefixer'); -const { marked } = require('marked'); -const postcss = require('postcss'); -const sass = require('sass'); -const junk = require('junk'); -const semver = require('semver'); -const replace = require('metalsmith-one-replace'); -const fsExtra = require('fs-extra'); - -const githubLinks = require('./scripts/plugins/githubLinks'); -const navigation = require('./scripts/plugins/navigation'); -const anchorMarkdownHeadings = require('./scripts/plugins/anchor-markdown-headings'); -const loadVersions = require('./scripts/load-versions'); -const latestVersion = require('./scripts/helpers/latestversion'); -const withPreserveLocale = require('./scripts/plugins/withPreserveLocale'); -const scriptReg = require('./scripts/plugins/scriptReg'); -const hbsReg = require('./scripts/plugins/hbsReg'); - -// Set the default language, also functions as a fallback for properties which -// are not defined in the given language. -const DEFAULT_LANG = 'en'; - -// The history links of nodejs versions at doc/index.md -const nodejsVersionsContent = fs - .readFileSync('./source/nodejsVersions.md') - .toString(); - -// Set up the Markdown renderer that we'll use for our Metalsmith build process. -const renderer = new marked.Renderer(); -renderer.heading = anchorMarkdownHeadings; -const markedOptions = { - renderer -}; - -// We are setting the output from `latestVersion` module here for future use. -// available props `latestVersionInfo` are `current` and `lts` -let latestVersionInfo = {}; - -// This function imports a given language file and uses the default language set -// in DEFAULT_LANG as a fallback to prevent any strings that aren't filled out -// from appearing as blank. -function i18nJSON(lang) { - const defaultJSON = require(`./locale/${DEFAULT_LANG}/site.json`); - const templateJSON = require(`./locale/${lang}/site.json`); - - return defaultsDeep({}, templateJSON, defaultJSON); -} - -// This function imports language file for each given locale in array 'localesList' -// and based on it generating locales data, which includes full language name, english language name, locale and link -function generateLocalesData(localesList) { - return localesList.map((localeEl) => { - const { - language, - languageEnglishVersion, - locale, - url - } = require(`./locale/${localeEl}/site.json`); - return { language, locale, url, languageEnglishVersion }; - }); -} - -// This is the function where the actual magic happens. This contains the main -// Metalsmith build cycle used for building a locale subsite, such as the -// english one. -function buildLocale(source, locale, opts) { - console.log(`[metalsmith] build/${locale} started`); - const labelForBuild = `[metalsmith] build/${locale} finished`; - console.time(labelForBuild); - const metalsmith = Metalsmith(__dirname); - metalsmith - // Sets global metadata imported from the locale's respective site.json. - .metadata({ - site: i18nJSON(locale), - project: source.project, - locales: opts.localesData - }) - // Sets the build source as the locale folder. - .source(path.join(__dirname, 'locale', locale)) - // site.json files aren't needed in the output dir - .ignore('site.json') - .use(withPreserveLocale(opts && opts.preserveLocale)) - // Extracts the main menu and sub-menu links form locale's site.json and - // adds them to the metadata. This data is used in the navigation template - .use(navigation(source.project.latestVersions)) - // Defines the blog post/guide collections used to internally group them for - // easier future handling and feed generation. - .use( - collections({ - blog: { - pattern: 'blog/**/*.md', - sortBy: 'date', - reverse: true, - refer: false - }, - blogReleases: { - pattern: 'blog/release/*.md', - sortBy: 'date', - reverse: true, - refer: false - }, - blogVulnerability: { - pattern: 'blog/vulnerability/*.md', - sortBy: 'date', - reverse: true, - refer: false - }, - knowledgeBase: { - pattern: 'knowledge/**/*.md', - refer: false - }, - guides: { - pattern: 'docs/guides/!(index).md' - } - }) - ) - .use( - pagination({ - path: 'blog/year', - iteratee: (post, idx) => ({ - post - }) - }) - ) - .use( - replace({ - actions: [ - { - type: 'var', - varValues: { - currentVersion: `latest-${latestVersionInfo.lts.nodeMajor}`, - nodeVersionLinks: nodejsVersionsContent - } - } - ] - }) - ) - .use(markdown(markedOptions)) - // Set pretty permalinks, we don't want .html suffixes everywhere. - .use( - permalinks({ - relative: false - }) - ) - // Generates the feed XML files from their respective collections which were - // defined earlier on. - .use( - feed({ - collection: 'blog', - destination: 'feed/blog.xml', - title: 'Node.js Blog' - }) - ) - .use( - feed({ - collection: 'blogReleases', - destination: 'feed/releases.xml', - title: 'Node.js Blog: Releases' - }) - ) - .use( - feed({ - collection: 'blogVulnerability', - destination: 'feed/vulnerability.xml', - title: 'Node.js Blog: Vulnerability Reports' - }) - ) - // Finally, this compiles the rest of the layouts present in ./layouts. - // They're language-agnostic, but have to be regenerated for every locale - // anyways. - .use(hbsReg()) - .use(scriptReg()) - .use(layouts()) - .use(githubLinks({ locale, site: i18nJSON(locale) })) - // Pipes the generated files into their respective subdirectory in the build - // directory. - .destination(path.join(__dirname, 'build', locale)) - // This actually executes the build and stops the internal timer after - // completion. - .build((err) => { - if (err) { - throw err; - } - console.timeEnd(labelForBuild); - }); -} - -// This function builds the static/css folder for all the Sass files. -async function buildCSS() { - console.log('[sass] static/css started'); - const labelForBuild = '[sass] static/css finished'; - console.time(labelForBuild); - - const src = path.join(__dirname, 'layouts/css/styles.scss'); - const sassOpts = { - outputStyle: - process.env.NODE_ENV !== 'development' ? 'compressed' : 'expanded' - }; - - const resultPromise = sass.compileAsync(src, sassOpts); - - const dest = path.join(__dirname, 'build/static/css/styles.css'); - - await fsExtra.promises.mkdir(path.join(__dirname, 'build/static/css'), { - recursive: true - }); - - const result = await resultPromise; - - postcss([autoprefixer]) - .process(result.css, { from: src }) - .then(async (res) => { - res.warnings().forEach((warn) => { - console.warn(warn.toString()); - }); - - await fsExtra.writeFile(dest, res.css); - console.timeEnd(labelForBuild); - }); -} - -// This function copies the rest of the static assets to their subfolder in the -// build directory. -async function copyStatic() { - console.log('[fsExtra] copy/static started'); - const labelForBuild = '[fsExtra] copy/static finished'; - console.time(labelForBuild); - - await fsExtra.promises.mkdir(path.join(__dirname, 'build/static/js'), { - recursive: true - }); - - await Promise.all([ - fsExtra.copy( - path.join(__dirname, 'static'), - path.join(__dirname, 'build/static'), - { overwrite: true, recursive: true } - ), - - fsExtra.copy( - path.join( - __dirname, - 'node_modules/jquery.fancytable/dist/fancyTable.min.js' - ), - path.join(__dirname, 'build/static/js/fancyTable.min.js'), - { overwrite: true } - ), - - fsExtra.copy( - path.join(__dirname, 'node_modules/jquery/dist/jquery.min.js'), - path.join(__dirname, 'build/static/js/jquery.min.js'), - { overwrite: true } - ) - ]); - - console.timeEnd(labelForBuild); -} - -function getSource(callback) { - // Loads all node/io.js versions. - loadVersions((err, versions) => { - latestVersionInfo = { - current: latestVersion.current(versions), - lts: latestVersion.lts(versions) - }; - const source = { - project: { - versions, - latestVersions: latestVersionInfo - } - }; - if ( - semver.gt( - source.project.latestVersions.lts.node, - source.project.latestVersions.current.node - ) - ) { - // If LTS is higher than Current hide it from the main page - source.project.latestVersions.hideCurrent = true; - } - - callback(err, source); - }); -} - -// This is where the build is orchestrated from, as indicated by the function -// name. It brings together all build steps and dependencies and executes them. -async function fullBuild(opts) { - const { selectedLocales, preserveLocale } = opts; - getSource(async (err, source) => { - if (err) { - throw err; - } - const locales = await fsExtra.promises.readdir( - path.join(__dirname, 'locale') - ); - - const filteredLocales = locales.filter( - (file) => - junk.not(file) && - (selectedLocales ? selectedLocales.includes(file) : true) - ); - const localesData = generateLocalesData(filteredLocales); - filteredLocales.forEach((locale) => { - buildLocale(source, locale, { preserveLocale, localesData }); - }); - }); -} - -// Starts the build if the file was executed from the command line -if (require.main === module) { - const preserveLocale = process.argv.includes('--preserveLocale'); - const selectedLocales = process.env.DEFAULT_LOCALE - ? process.env.DEFAULT_LOCALE.toLowerCase().split(',') - : process.env.DEFAULT_LOCALE; - // Copy static files - copyStatic(); - // Build CSS - buildCSS(); - fullBuild({ selectedLocales, preserveLocale }); -} - -exports.getSource = getSource; -exports.fullBuild = fullBuild; -exports.buildCSS = buildCSS; -exports.buildLocale = buildLocale; -exports.copyStatic = copyStatic; -exports.generateLocalesData = generateLocalesData; diff --git a/components/AnchoredHeading.tsx b/components/AnchoredHeading.tsx new file mode 100644 index 0000000000000..c19bc9cd7f638 --- /dev/null +++ b/components/AnchoredHeading.tsx @@ -0,0 +1,60 @@ +import type { FC, PropsWithChildren } from 'react'; + +type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6; + +type AnchorHeadingProps = PropsWithChildren<{ + level: HeadingLevel; + id?: string; +}>; + +/** + * This module will automatically replace the header style words + * (such as `#`,`##`...ect) to an anchor name. + * + * This processor will make all the English characters (26 letters) + * into their lower case, for others in the middle of words, + * they will become `-` in default. For more than one `-` together, + * there's only one left. + * + * If your title has some non-English characters, please use + * `` to quote your English anchor name inside, with your + * own title beside it. Otherwises it will return you what it is. + */ + +// React will ignore "" in the string, +// so we can just use '-- --' to quote the anchor name inside it. +const COMMENT_FOR_HEADANCHOR = /--\x20?([\w\x20-]+)\x20?--/; + +const AnchoredHeading: FC = ({ level, id, children }) => { + const HeadingLevelTag = `h${level}` as any; + + let sanitizedId = + id ?? children?.toLocaleString().toLocaleLowerCase().replace(/\x20/g, '-'); + + if (sanitizedId) { + const foundAnchorAndTitle = COMMENT_FOR_HEADANCHOR.exec(sanitizedId); + + if (foundAnchorAndTitle) { + sanitizedId = foundAnchorAndTitle[1] + .replace(/(\[([^\]]+)]\([^)]+\))/g, '$2') + .replace(/[^\w]+/g, '-') + .replace(/[\x20]+/g, '-') + .replace(/-{2,}/g, '-') + .replace(/(^-|-$)/g, ''); + } + } + + return ( + + {children} + + + ); +}; + +export default AnchoredHeading; diff --git a/components/Api/ApiChanges/__snapshots__/index.stories.tsx.snap b/components/Api/ApiChanges/__snapshots__/index.stories.tsx.snap new file mode 100644 index 0000000000000..b3d126a6111f8 --- /dev/null +++ b/components/Api/ApiChanges/__snapshots__/index.stories.tsx.snap @@ -0,0 +1,41 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Api/ApiChanges Default smoke-test 1`] = ` +
+ + + History + + + + + + + + + + + + + + + + + + + +
+ Version + + Changes +
+ v10.0.0 + + test +
+ v10.0.0 + + Added in: v10.0.0 +
+
+`; diff --git a/components/Api/ApiChanges/index.module.scss b/components/Api/ApiChanges/index.module.scss new file mode 100644 index 0000000000000..7ef2c66a61cf1 --- /dev/null +++ b/components/Api/ApiChanges/index.module.scss @@ -0,0 +1,10 @@ +.changesComponent { + cursor: pointer; + + strong { + display: inline-block; + font-size: 0.67em; + margin: 0; + text-transform: uppercase; + } +} diff --git a/components/Api/ApiChanges/index.stories.tsx b/components/Api/ApiChanges/index.stories.tsx new file mode 100644 index 0000000000000..320a0e4f01844 --- /dev/null +++ b/components/Api/ApiChanges/index.stories.tsx @@ -0,0 +1,24 @@ +import ApiChanges from './index'; +import type { Meta as MetaObj, StoryObj } from '@storybook/react'; + +type Story = StoryObj; +type Meta = MetaObj; + +const version = 'v10.0.0'; +export const Default: Story = { + args: { + update: { + type: 'added', + version: [version], + }, + changes: [ + { + version: version, + 'pr-url': 'https://github.com/nodejs/node/pulls', + description: 'test', + }, + ], + }, +}; + +export default { component: ApiChanges } as Meta; diff --git a/components/Api/ApiChanges/index.tsx b/components/Api/ApiChanges/index.tsx new file mode 100644 index 0000000000000..79498c602c30c --- /dev/null +++ b/components/Api/ApiChanges/index.tsx @@ -0,0 +1,51 @@ +import { FormattedMessage } from 'react-intl'; +import styles from './index.module.scss'; +import { parseApiDocsVersion } from '../../../util/parseApiDocsVersion'; +import type { ApiChange, ApiUpdate } from '../../../types'; +import type { FC } from 'react'; + +type ApiChangesProps = { + update: ApiUpdate; + changes: ApiChange[]; +}; + +const ApiChanges: FC = ({ update, changes }) => ( +
+ + + + + + + + + + + + + + {changes.map(({ version, description }) => ( + + + + + ))} + + + + + +
+ + + +
{parseApiDocsVersion(version)}{description}
{parseApiDocsVersion(update.version)} + +
+
+); + +export default ApiChanges; diff --git a/components/Api/DataTag/__snapshots__/index.stories.ts.snap b/components/Api/DataTag/__snapshots__/index.stories.ts.snap new file mode 100644 index 0000000000000..4cd2f15367a50 --- /dev/null +++ b/components/Api/DataTag/__snapshots__/index.stories.ts.snap @@ -0,0 +1,25 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Api/DataTag Blue smoke-test 1`] = ` + + M + +`; + +exports[`Api/DataTag Red smoke-test 1`] = ` + + E + +`; + +exports[`Api/DataTag Yellow smoke-test 1`] = ` + + C + +`; diff --git a/components/Api/DataTag/index.module.scss b/components/Api/DataTag/index.module.scss new file mode 100644 index 0000000000000..ce7fcd95408f9 --- /dev/null +++ b/components/Api/DataTag/index.module.scss @@ -0,0 +1,26 @@ +.dataTag { + border-radius: 50%; + color: #fff; + display: inline-block; + font-family: var(--sans); + font-size: 1.4rem; + font-weight: var(--font-weight-light); + height: 2.4rem; + line-height: 2.4rem; + margin-right: 0.8rem; + min-width: 2.4rem; + text-align: center; + width: 2.4rem; + + &[data-tag='C'] { + background-color: var(--warning4); + } + + &[data-tag='E'] { + background-color: var(--danger6); + } + + &[data-tag='M'] { + background-color: var(--info6); + } +} diff --git a/components/Api/DataTag/index.stories.ts b/components/Api/DataTag/index.stories.ts new file mode 100644 index 0000000000000..fda26da0f4f1c --- /dev/null +++ b/components/Api/DataTag/index.stories.ts @@ -0,0 +1,25 @@ +import DataTag from './index'; +import type { Meta as MetaObj, StoryObj } from '@storybook/react'; + +type Story = StoryObj; +type Meta = MetaObj; + +export const Red: Story = { + args: { + tag: 'E', + }, +}; + +export const Yellow: Story = { + args: { + tag: 'C', + }, +}; + +export const Blue: Story = { + args: { + tag: 'M', + }, +}; + +export default { component: DataTag } as Meta; diff --git a/components/Api/DataTag/index.tsx b/components/Api/DataTag/index.tsx new file mode 100644 index 0000000000000..049d03f9ab2a5 --- /dev/null +++ b/components/Api/DataTag/index.tsx @@ -0,0 +1,12 @@ +import styles from './index.module.scss'; +import type { FC } from 'react'; + +type DataTagProps = { tag: 'E' | 'C' | 'M' }; + +const DataTag: FC = ({ tag }) => ( + + {tag} + +); + +export default DataTag; diff --git a/components/Api/JsonLink/__snapshots__/index.stories.ts.snap b/components/Api/JsonLink/__snapshots__/index.stories.ts.snap new file mode 100644 index 0000000000000..0d229a4b63323 --- /dev/null +++ b/components/Api/JsonLink/__snapshots__/index.stories.ts.snap @@ -0,0 +1,23 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Api/JsonLink Default smoke-test 1`] = ` +
+`; diff --git a/components/Api/JsonLink/index.module.scss b/components/Api/JsonLink/index.module.scss new file mode 100644 index 0000000000000..b1e23af5b893b --- /dev/null +++ b/components/Api/JsonLink/index.module.scss @@ -0,0 +1,29 @@ +.json { + display: flex; + flex-wrap: wrap; + + a { + color: var(--color-text-secondary); + font-family: var(--sans-serif); + font-size: 1.4rem; + font-weight: var(--font-weight-regular); + margin-left: 0; + text-decoration: none !important; + text-transform: uppercase; + vertical-align: middle; + + span { + font-weight: var(--font-weight-regular); + vertical-align: middle; + } + + &:hover { + color: var(--brand-light); + } + + .FaRobotIcon { + margin-left: 0.5rem; + vertical-align: middle; + } + } +} diff --git a/components/Api/JsonLink/index.stories.ts b/components/Api/JsonLink/index.stories.ts new file mode 100644 index 0000000000000..a2bbbdcc01186 --- /dev/null +++ b/components/Api/JsonLink/index.stories.ts @@ -0,0 +1,14 @@ +import JsonLink from './index'; +import type { Meta as MetaObj, StoryObj } from '@storybook/react'; + +type Story = StoryObj; +type Meta = MetaObj; + +export const Default: Story = { + args: { + version: 'v18', + fileName: 'documentation', + }, +}; + +export default { component: JsonLink } as Meta; diff --git a/components/Api/JsonLink/index.tsx b/components/Api/JsonLink/index.tsx new file mode 100644 index 0000000000000..4ebf727435360 --- /dev/null +++ b/components/Api/JsonLink/index.tsx @@ -0,0 +1,22 @@ +import { FormattedMessage } from 'react-intl'; +import { FaRobot } from 'react-icons/fa'; +import { DOCS_URL } from '../../../next.constants.mjs'; +import type { FC } from 'react'; + +import styles from './index.module.scss'; + +type JsonLinkProps = { + fileName: string; + version: string; +}; + +const JsonLink: FC = ({ fileName, version }) => ( + +); + +export default JsonLink; diff --git a/components/Api/SourceLink/__snapshots__/index.stories.ts.snap b/components/Api/SourceLink/__snapshots__/index.stories.ts.snap new file mode 100644 index 0000000000000..6ee8ce731c797 --- /dev/null +++ b/components/Api/SourceLink/__snapshots__/index.stories.ts.snap @@ -0,0 +1,10 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Api/SourceLink Default smoke-test 1`] = ` + +`; diff --git a/components/Api/SourceLink/index.module.scss b/components/Api/SourceLink/index.module.scss new file mode 100644 index 0000000000000..afac99ee67501 --- /dev/null +++ b/components/Api/SourceLink/index.module.scss @@ -0,0 +1,5 @@ +.sourceLinkComponent { + font-size: var(--font-size-body3); + font-weight: var(--font-weight-semibold); + text-transform: uppercase; +} diff --git a/components/Api/SourceLink/index.stories.ts b/components/Api/SourceLink/index.stories.ts new file mode 100644 index 0000000000000..41848191afc79 --- /dev/null +++ b/components/Api/SourceLink/index.stories.ts @@ -0,0 +1,14 @@ +import SourceLink from './index'; +import type { Meta as MetaObj, StoryObj } from '@storybook/react'; + +type Story = StoryObj; +type Meta = MetaObj; + +export const Default: Story = { + args: { + version: '1.0.0', + link: 'http://nodejs.org/version/1.0.0', + }, +}; + +export default { component: SourceLink } as Meta; diff --git a/components/Api/SourceLink/index.tsx b/components/Api/SourceLink/index.tsx new file mode 100644 index 0000000000000..da253e670ebc1 --- /dev/null +++ b/components/Api/SourceLink/index.tsx @@ -0,0 +1,20 @@ +import { FormattedMessage } from 'react-intl'; + +import styles from './index.module.scss'; +import type { FC } from 'react'; + +type SourceLinkProps = { + link: string; + version: string; +}; + +const SourceLink: FC = ({ version, link }) => ( +

+ {' '} + + {link} + +

+); + +export default SourceLink; diff --git a/components/Api/Stability/__snapshots__/index.stories.tsx.snap b/components/Api/Stability/__snapshots__/index.stories.tsx.snap new file mode 100644 index 0000000000000..834e39b59d858 --- /dev/null +++ b/components/Api/Stability/__snapshots__/index.stories.tsx.snap @@ -0,0 +1,7 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Api/Stability Default smoke-test 1`] = ` +
+ Stability: 0 - This is an stability index example +
+`; diff --git a/components/Api/Stability/index.module.scss b/components/Api/Stability/index.module.scss new file mode 100644 index 0000000000000..e375fc8dc780f --- /dev/null +++ b/components/Api/Stability/index.module.scss @@ -0,0 +1,36 @@ +.stability { + border-radius: 4px; + color: #fff; + line-height: 1.5; + margin: 0 0 1rem; + padding: 1rem; + + p { + display: inline; + margin: 0; + } + + a { + color: #fff; + + code { + color: #fff !important; + } + } + + &Level0 { + background-color: var(--danger5); + } + + &Level1 { + background-color: var(--warning4); + } + + &Level2 { + background-color: var(--brand5); + } + + &Level3 { + background-color: var(--info5); + } +} diff --git a/components/Api/Stability/index.stories.tsx b/components/Api/Stability/index.stories.tsx new file mode 100644 index 0000000000000..aca5db7075a29 --- /dev/null +++ b/components/Api/Stability/index.stories.tsx @@ -0,0 +1,24 @@ +import Stability from './index'; +import type { Meta as MetaObj, StoryObj } from '@storybook/react'; + +type Story = StoryObj; +type Meta = MetaObj; + +export const Default: Story = { + args: { + stability: 0, + children: 'This is an stability index example', + }, + argTypes: { + stability: { + control: { + type: 'range', + min: 0, + max: 3, + step: 1, + }, + }, + }, +}; + +export default { component: Stability } as Meta; diff --git a/components/Api/Stability/index.tsx b/components/Api/Stability/index.tsx new file mode 100644 index 0000000000000..44f6bb00c3cdc --- /dev/null +++ b/components/Api/Stability/index.tsx @@ -0,0 +1,23 @@ +import { FormattedMessage } from 'react-intl'; +import styles from './index.module.scss'; +import type { PropsWithChildren, FC } from 'react'; + +type StabilityProps = PropsWithChildren<{ stability: number }>; + +const getStabilityClass = (stability: number) => { + const style = styles[`stabilityLevel${stability}`]; + if (!style) throw new Error(`Unknown stability level: ${stability}`); + return style; +}; + +const Stability: FC = ({ stability, children }) => ( +
+ + {children} +
+); + +export default Stability; diff --git a/components/Article/Alert/__snapshots__/index.stories.ts.snap b/components/Article/Alert/__snapshots__/index.stories.ts.snap new file mode 100644 index 0000000000000..6b426fa60a183 --- /dev/null +++ b/components/Article/Alert/__snapshots__/index.stories.ts.snap @@ -0,0 +1,7 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Article/Alert Default smoke-test 1`] = ` +
+ This is an alert +
+`; diff --git a/components/Article/Alert/index.module.scss b/components/Article/Alert/index.module.scss new file mode 100644 index 0000000000000..33a3b989967e1 --- /dev/null +++ b/components/Article/Alert/index.module.scss @@ -0,0 +1,16 @@ +.alert { + background: var(--purple5); + border-radius: 5px; + color: var(--color-fill-top-nav); + font-size: var(--font-size-body1); + font-weight: var(--font-weight-bold); + margin: var(--space-16) auto; + max-width: 90vw; + padding: var(--space-12); + position: relative; + + a, + a:hover { + color: white; + } +} diff --git a/components/Article/Alert/index.stories.ts b/components/Article/Alert/index.stories.ts new file mode 100644 index 0000000000000..cc8b0652d01b0 --- /dev/null +++ b/components/Article/Alert/index.stories.ts @@ -0,0 +1,13 @@ +import Alert from './index'; +import type { Meta as MetaObj, StoryObj } from '@storybook/react'; + +type Story = StoryObj; +type Meta = MetaObj; + +export const Default: Story = { + args: { + children: 'This is an alert', + }, +}; + +export default { component: Alert } as Meta; diff --git a/components/Article/Alert/index.tsx b/components/Article/Alert/index.tsx new file mode 100644 index 0000000000000..fc00b6c16ffad --- /dev/null +++ b/components/Article/Alert/index.tsx @@ -0,0 +1,8 @@ +import styles from './index.module.scss'; +import type { FC, PropsWithChildren } from 'react'; + +const Alert: FC = ({ children }) => ( +
{children}
+); + +export default Alert; diff --git a/components/Article/AuthorList/Author/__snapshots__/index.stories.tsx.snap b/components/Article/AuthorList/Author/__snapshots__/index.stories.tsx.snap new file mode 100644 index 0000000000000..4320ab68a0c40 --- /dev/null +++ b/components/Article/AuthorList/Author/__snapshots__/index.stories.tsx.snap @@ -0,0 +1,39 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Article/AuthorList/Author Default smoke-test 1`] = ` + + nodejs + +`; + +exports[`Article/AuthorList/Author WithourUsername smoke-test 1`] = ` + + + +`; diff --git a/components/Article/AuthorList/Author/index.module.scss b/components/Article/AuthorList/Author/index.module.scss new file mode 100644 index 0000000000000..416b6e8a802ec --- /dev/null +++ b/components/Article/AuthorList/Author/index.module.scss @@ -0,0 +1,16 @@ +.link { + &:hover img { + transform: scale(1.1); + } + + img { + background-size: contain; + border: 2px solid var(--brand-light); + border-radius: 100%; + display: block; + height: 30px; + margin-top: 5px; + transition: all 0.2s ease-in-out; + width: 30px; + } +} diff --git a/components/Article/AuthorList/Author/index.stories.tsx b/components/Article/AuthorList/Author/index.stories.tsx new file mode 100644 index 0000000000000..1384b2d08e898 --- /dev/null +++ b/components/Article/AuthorList/Author/index.stories.tsx @@ -0,0 +1,21 @@ +import Author from './index'; +import type { Meta as MetaObj, StoryObj } from '@storybook/react'; + +type Story = StoryObj; +type Meta = MetaObj; + +export const Default: Story = { + args: { + username: 'nodejs', + size: 60, + }, +}; + +export const WithourUsername: Story = { + args: { + username: '', + size: 0, + }, +}; + +export default { component: Author } as Meta; diff --git a/components/Article/AuthorList/Author/index.tsx b/components/Article/AuthorList/Author/index.tsx new file mode 100644 index 0000000000000..2c7f19bdcc7bb --- /dev/null +++ b/components/Article/AuthorList/Author/index.tsx @@ -0,0 +1,45 @@ +import { useState } from 'react'; +import { useIntl } from 'react-intl'; +import Image from 'next/image'; +import styles from './index.module.scss'; +import type { FC } from 'react'; + +type AuthorProps = { username: string; size?: number }; + +const Author: FC = ({ username, size }) => { + const githubUserName = username.trim(); + const githubLink = `https://github.com/${githubUserName}`; + const githubImgLink = `https://github.com/${githubUserName}.png?size=${size}`; + + const intl = useIntl(); + + const [authorImg, setAuthorImg] = useState(githubImgLink); + + const translation = intl.formatMessage( + { id: 'components.article.author.githubLinkLabel' }, + { username } + ); + + return ( + + {githubUserName} setAuthorImg('/placeholder-img.png')} + /> + + ); +}; + +export default Author; diff --git a/components/Article/AuthorList/__snapshots__/index.stories.tsx.snap b/components/Article/AuthorList/__snapshots__/index.stories.tsx.snap new file mode 100644 index 0000000000000..126657f79057c --- /dev/null +++ b/components/Article/AuthorList/__snapshots__/index.stories.tsx.snap @@ -0,0 +1,81 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Article/AuthorList Default smoke-test 1`] = ` +
+ Article Authors + +
+`; diff --git a/components/Article/AuthorList/index.module.scss b/components/Article/AuthorList/index.module.scss new file mode 100644 index 0000000000000..2d3c3c4b47f0a --- /dev/null +++ b/components/Article/AuthorList/index.module.scss @@ -0,0 +1,26 @@ +.authorList { + color: var(--color-text-secondary); + display: flex; + flex-direction: column; + font-size: var(--font-size-body2); + font-weight: var(--font-weight-bold); + margin-bottom: var(--space-24); + max-width: 600px; + text-transform: uppercase; + + ul { + display: flex; + flex-wrap: wrap; + list-style: none; + margin: 0; + padding: 0; + + li { + margin: 0.5rem 0.5rem 0.5rem 0; + + &:first-of-type a { + margin-left: 0; + } + } + } +} diff --git a/components/Article/AuthorList/index.stories.tsx b/components/Article/AuthorList/index.stories.tsx new file mode 100644 index 0000000000000..2f82bebbdd837 --- /dev/null +++ b/components/Article/AuthorList/index.stories.tsx @@ -0,0 +1,13 @@ +import AuthorList from './index'; +import type { Meta as MetaObj, StoryObj } from '@storybook/react'; + +type Story = StoryObj; +type Meta = MetaObj; + +export const Default: Story = { + args: { + authors: ['flaviocopes', 'MarkPieszak', 'mcollina', 'unavailable-author'], + }, +}; + +export default { component: AuthorList } as Meta; diff --git a/components/Article/AuthorList/index.tsx b/components/Article/AuthorList/index.tsx new file mode 100644 index 0000000000000..633e08cdba7a8 --- /dev/null +++ b/components/Article/AuthorList/index.tsx @@ -0,0 +1,27 @@ +import { FormattedMessage } from 'react-intl'; +import Author from './Author'; +import styles from './index.module.scss'; +import type { FC } from 'react'; + +type AuthorListProps = { authors: string[] }; + +const AuthorList: FC = ({ authors }) => { + if (authors.length) { + return ( +
+ +
    + {authors.map(author => ( +
  • + +
  • + ))} +
+
+ ); + } + + return null; +}; + +export default AuthorList; diff --git a/components/Article/BlockQuote/__snapshots__/index.stories.tsx.snap b/components/Article/BlockQuote/__snapshots__/index.stories.tsx.snap new file mode 100644 index 0000000000000..af7ead45bcdb4 --- /dev/null +++ b/components/Article/BlockQuote/__snapshots__/index.stories.tsx.snap @@ -0,0 +1,18 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Article/BlockQuote Default smoke-test 1`] = ` +
+ This is a block quote +
+`; + +exports[`Article/BlockQuote MultipleParagraph smoke-test 1`] = ` +
+

+ This is a block quote 1 +

+

+ This is a block quote 2 +

+
+`; diff --git a/components/Article/BlockQuote/index.module.scss b/components/Article/BlockQuote/index.module.scss new file mode 100644 index 0000000000000..5874a1fc7db72 --- /dev/null +++ b/components/Article/BlockQuote/index.module.scss @@ -0,0 +1,22 @@ +.blockQuote { + background-color: var(--color-fill-banner); + border-radius: 5px; + color: var(--color-text-primary); + font-size: var(--font-size-body1); + margin: var(--space-16) auto; + max-width: 90vw; + padding: var(--space-12); + position: relative; + + @media (max-width: 900px) { + margin: var(--space-08) auto; + } + + p:first-child { + margin-top: 0; + } + + p:last-child { + margin: 0; + } +} diff --git a/components/Article/BlockQuote/index.stories.tsx b/components/Article/BlockQuote/index.stories.tsx new file mode 100644 index 0000000000000..10de043853705 --- /dev/null +++ b/components/Article/BlockQuote/index.stories.tsx @@ -0,0 +1,20 @@ +import BlockQuote from './index'; +import type { Meta as MetaObj, StoryObj } from '@storybook/react'; + +type Story = StoryObj; +type Meta = MetaObj; + +export const Default: Story = { + args: { children: 'This is a block quote' }, +}; + +export const MultipleParagraph: Story = { + args: { + children: [ +

This is a block quote 1

, +

This is a block quote 2

, + ], + }, +}; + +export default { component: BlockQuote } as Meta; diff --git a/components/Article/BlockQuote/index.tsx b/components/Article/BlockQuote/index.tsx new file mode 100644 index 0000000000000..47681f83c5901 --- /dev/null +++ b/components/Article/BlockQuote/index.tsx @@ -0,0 +1,8 @@ +import styles from './index.module.scss'; +import type { FC, PropsWithChildren } from 'react'; + +const BlockQuote: FC = ({ children }) => ( +
{children}
+); + +export default BlockQuote; diff --git a/components/Article/Codebox/__snapshots__/index.stories.tsx.snap b/components/Article/Codebox/__snapshots__/index.stories.tsx.snap new file mode 100644 index 0000000000000..91223627a3c87 --- /dev/null +++ b/components/Article/Codebox/__snapshots__/index.stories.tsx.snap @@ -0,0 +1,73 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Article/Codebox Default smoke-test 1`] = ` +
+  
+
+ +
+ +
+
+ + const + + a + + = + + + 1 + + + ; + +
+
+`; + +exports[`Article/Codebox MultiLang smoke-test 1`] = ` +
+  
+
+ + +
+ +
+
+ + const + + http + + = + + + require + + + ( + + + 'http' + + + ) + + + ; + +
+
+`; diff --git a/components/Article/Codebox/__tests__/__snapshots__/index.test.tsx.snap b/components/Article/Codebox/__tests__/__snapshots__/index.test.tsx.snap new file mode 100644 index 0000000000000..7783af5875145 --- /dev/null +++ b/components/Article/Codebox/__tests__/__snapshots__/index.test.tsx.snap @@ -0,0 +1,140 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Codebox component (multiple langs) switch between languages 1`] = ` +
+
+    
+
+ + +
+ +
+
+ + const + + http + + = + + + + require + + + ( + + + 'http' + + + ) + + + ; + + + +
+
+
+`; + +exports[`Codebox component (multiple langs) switch between languages 2`] = ` +
+
+    
+
+ + +
+ +
+
+ + import + + http + + from + + + + 'http' + + + ; + +
+
+
+`; diff --git a/components/Article/Codebox/__tests__/index.test.tsx b/components/Article/Codebox/__tests__/index.test.tsx new file mode 100644 index 0000000000000..963ca7d798603 --- /dev/null +++ b/components/Article/Codebox/__tests__/index.test.tsx @@ -0,0 +1,71 @@ +import userEvent from '@testing-library/user-event'; +import { render, screen } from '@testing-library/react'; +import { IntlProvider } from 'react-intl'; + +import Codebox, { replaceLabelLanguages, replaceLanguages } from '../index'; + +describe('Replacer tests', (): void => { + it('replaceLabelLanguages', (): void => { + expect(replaceLabelLanguages('language-console')).toBe('language-bash'); + }); + + it('replaceLanguages', (): void => { + expect(replaceLanguages('language-mjs')).toBe('language-js'); + expect(replaceLanguages('language-cjs')).toBe('language-js'); + expect(replaceLanguages('language-javascript')).toBe('language-js'); + expect(replaceLanguages('language-console')).toBe('language-bash'); + expect(replaceLanguages('language-shell')).toBe('language-bash'); + }); +}); + +describe('Codebox component (one lang)', (): void => { + const code = 'const a = 1;'; + + it('should copy content', async () => { + const user = userEvent.setup(); + + render( + {}}> + +
{code}
+
+
+ ); + + const navigatorClipboardWriteTextSpy = jest.spyOn( + navigator.clipboard, + 'writeText' + ); + + const buttonElement = screen.getByText('components.codeBox.copy'); + await user.click(buttonElement); + + expect(navigatorClipboardWriteTextSpy).toHaveBeenCalledTimes(1); + expect(navigatorClipboardWriteTextSpy).toHaveBeenCalledWith(code); + }); +}); + +describe('Codebox component (multiple langs)', (): void => { + const code = `const http = require('http'); +-------------- +import http from 'http';`; + + it('switch between languages', async () => { + const user = userEvent.setup(); + + const { container } = render( + {}}> + +
{code}
+
+
+ ); + + expect(container).toMatchSnapshot(); + + const buttonElement = await screen.findByText('mjs'); + await user.click(buttonElement); + + expect(container).toMatchSnapshot(); + }); +}); diff --git a/components/Article/Codebox/index.module.scss b/components/Article/Codebox/index.module.scss new file mode 100644 index 0000000000000..598f996cc077f --- /dev/null +++ b/components/Article/Codebox/index.module.scss @@ -0,0 +1,99 @@ +@use 'themes/light'; +@use 'themes/dark'; +@use 'styles/mixins/components'; + +.pre { + @include components.code; + + .top { + display: flex; + flex-direction: row; + justify-content: space-between; + + .lang, + .copy { + align-items: center; + cursor: pointer; + display: inherit; + font-size: var(--font-size-code); + height: 23px; + justify-content: center; + width: 86px; + } + + .langBox { + background-color: var(--black4); + border-radius: 0 0 0.3rem 0; + display: flex; + flex-direction: row; + justify-content: center; + + .lang { + background-color: var(--black4); + border-width: 0; + color: var(--black9); + padding: 0 16px; + width: max-content; + + &:last-of-type { + border-radius: 0 0 0.3rem 0; + } + + &:hover { + background-color: var(--black5); + } + } + + .lang.selected { + font-weight: 600; + } + } + + .copy { + background-color: var(--black9); + border-radius: 0 0 0 0.3rem; + border-width: 0; + color: white; + + &:hover { + background-color: var(--black8); + } + + i { + padding: 0; + } + } + } + + .content { + margin: 1em; + } +} + +[data-theme='light'] { + .pre { + @include light.theme; + } +} + +[data-theme='dark'] { + .pre { + @include dark.theme; + + .top { + span { + background-color: var(--black3); + color: var(--black9); + } + + .copy { + background-color: var(--brand8); + color: white; + + &:hover { + background-color: var(--brand6); + } + } + } + } +} diff --git a/components/Article/Codebox/index.stories.tsx b/components/Article/Codebox/index.stories.tsx new file mode 100644 index 0000000000000..94b48832d64bf --- /dev/null +++ b/components/Article/Codebox/index.stories.tsx @@ -0,0 +1,37 @@ +import Codebox from './index'; +import type { Meta as MetaObj, StoryObj } from '@storybook/react'; +import type { FC } from 'react'; + +type DecoratedCodeBoxProps = { language: string[]; code: string[] }; + +const DecoratedCodeBox: FC = ({ language, code }) => ( + +
{code.join('--------------\n')}
+
+); + +type Story = StoryObj; +type Meta = MetaObj; + +const singleLangCode = ['const a = 1;']; + +export const Default: Story = { + args: { + language: ['language-js'], + code: singleLangCode, + }, +}; + +const multiLangCode = [ + "const http = require('http');", + "import http from 'http';", +]; + +export const MultiLang: Story = { + args: { + language: ['language-cjs', 'language-mjs'], + code: multiLangCode, + }, +}; + +export default { component: DecoratedCodeBox } as Meta; diff --git a/components/Article/Codebox/index.tsx b/components/Article/Codebox/index.tsx new file mode 100644 index 0000000000000..f7f1eddfd8bb0 --- /dev/null +++ b/components/Article/Codebox/index.tsx @@ -0,0 +1,82 @@ +import { useEffect, useState } from 'react'; +import { FormattedMessage } from 'react-intl'; +import { highlight, languages } from 'prismjs'; +import classnames from 'classnames'; +import styles from './index.module.scss'; +import { useCopyToClipboard } from '../../../hooks/useCopyToClipboard'; +import type { FC, PropsWithChildren, ReactElement, MouseEvent } from 'react'; + +type CodeBoxProps = { + children: ReactElement>; +}; + +export const replaceLabelLanguages = (language: string) => + language.replace(/console/i, 'bash'); + +export const replaceLanguages = (language: string) => + language + .replace(/mjs|cjs|javascript/i, 'js') + .replace(/console|shell/i, 'bash'); + +const Codebox: FC = ({ children: { props } }) => { + const [parsedCode, setParsedCode] = useState(''); + const [copied, copyText] = useCopyToClipboard(); + const [langIndex, setLangIndex] = useState(0); + + const className = props.className || 'text'; + + const languageOptions = className + .split('|') + .map(language => language.split('language-')[1]); + + const language = languageOptions[langIndex]; + + const codeArray = props.children + ? props.children.toString().split('--------------\n') + : ['']; + + const handleCopyCode = (event: MouseEvent) => { + event.preventDefault(); + copyText(codeArray[langIndex]); + }; + + useEffect(() => { + const parsedLanguage = replaceLanguages(language); + const prismLanguage = languages[parsedLanguage] || languages.text; + + setParsedCode( + highlight(codeArray[langIndex], prismLanguage, parsedLanguage) + ); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [langIndex]); + + return ( +
+      
+
+ {languageOptions.map((lang, index) => ( + + ))} +
+ +
+
+
+ ); +}; + +export default Codebox; diff --git a/components/Article/Codebox/themes/_dark.scss b/components/Article/Codebox/themes/_dark.scss new file mode 100644 index 0000000000000..6f1fdbd7d522f --- /dev/null +++ b/components/Article/Codebox/themes/_dark.scss @@ -0,0 +1,90 @@ +@mixin theme { + background: var(--black9); + color: var(--black2); + + :global .token { + &.comment, + &.prolog, + &.doctype, + &.cdata { + color: #8292a2; + } + + &.operator, + &.punctuation { + color: var(--black2); + } + + &.namespace { + opacity: 0.7; + } + + &.property, + &.tag, + &.constant, + &.symbol, + &.deleted { + color: #f92672; + } + + &.boolean { + color: #ae81ff; + } + + &.selector, + &.attr-name, + &.char, + &.builtin, + &.inserted { + color: #a6e22e; + } + + &.entity, + &.url, + .language-css &.string, + .style &.string, + &.variable { + color: #f8f8f2; + } + + &.atrule, + &.attr-value, + &.class-name { + color: #e6db74; + } + + &.function { + color: var(--warning3); + } + + &.string { + color: var(--brand3); + } + + &.keyword { + color: var(--info3); + } + + &.number { + color: var(--purple3); + } + + &.regex, + &.important { + color: #fd971f; + } + + &.important, + &.bold { + font-weight: var(--font-weight-bold); + } + + &.italic { + font-style: italic; + } + + &.entity { + cursor: help; + } + } +} diff --git a/components/Article/Codebox/themes/_light.scss b/components/Article/Codebox/themes/_light.scss new file mode 100644 index 0000000000000..4c24e9a299f2e --- /dev/null +++ b/components/Article/Codebox/themes/_light.scss @@ -0,0 +1,76 @@ +@mixin theme { + background: var(--black2); + color: black; + + :global .token { + &.comment, + &.prolog, + &.doctype, + &.cdata { + color: slategray; + } + + &.namespace { + opacity: 0.7; + } + + &.property, + &.tag, + &.boolean, + &.number, + &.constant, + &.symbol, + &.deleted { + color: #905; + } + + &.selector, + &.attr-name, + &.char, + &.builtin, + &.inserted { + color: #690; + } + + &.entity, + &.url { + background: hsla(0, 0%, 100%, 0.5); + color: #9a6e3a; + } + + &.atrule, + &.attr-value, + &.keyword { + color: #07a; + } + + &.function, + &.class-name { + color: #dd4a68; + } + + &.regex, + &.important, + &.variable { + color: #e90; + } + + &.important, + &.bold { + font-weight: var(--font-weight-vold); + } + &.italic { + font-style: italic; + } + + &.entity { + cursor: help; + } + + &.punctuation, + &.operator, + &.string { + background-color: var(--black2); + } + } +} diff --git a/components/Article/EditLink/__snapshots__/index.stories.tsx.snap b/components/Article/EditLink/__snapshots__/index.stories.tsx.snap new file mode 100644 index 0000000000000..67b912d211c9f --- /dev/null +++ b/components/Article/EditLink/__snapshots__/index.stories.tsx.snap @@ -0,0 +1,22 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Article/EditLink Default smoke-test 1`] = ` + +`; diff --git a/components/Article/EditLink/__tests__/index.test.tsx b/components/Article/EditLink/__tests__/index.test.tsx new file mode 100644 index 0000000000000..5178505c99e4c --- /dev/null +++ b/components/Article/EditLink/__tests__/index.test.tsx @@ -0,0 +1,34 @@ +import { render, screen } from '@testing-library/react'; +import { IntlProvider } from 'react-intl'; +import EditLink from './../index'; + +jest.mock('next/router', () => ({ + useRouter: jest.fn().mockImplementation(() => ({ + asPath: '', + })), +})); + +const absolutePath = + 'https://github.com/nodejs/nodejs.org/edit/major/website-redesign/pages/en/get-involved/contribute.md'; +const relativePath = 'get-involved/contribute.md'; +const editPath = 'pages/en/get-involved/contribute.md'; + +describe('EditLink component', () => { + it('produces correct relative path', () => { + render( + {}}> + + + ); + expect(screen.getByRole('link')).toHaveAttribute('href', absolutePath); + }); + + it('produces correct edit path', () => { + render( + {}}> + + + ); + expect(screen.getByRole('link')).toHaveAttribute('href', absolutePath); + }); +}); diff --git a/components/Article/EditLink/index.module.scss b/components/Article/EditLink/index.module.scss new file mode 100644 index 0000000000000..ae12a140d9d09 --- /dev/null +++ b/components/Article/EditLink/index.module.scss @@ -0,0 +1,30 @@ +.edit { + display: flex; + flex-wrap: wrap; + margin-top: var(--space-48); + + a { + color: var(--color-text-secondary); + font-family: var(--sans-serif); + font-size: var(--font-size-body2); + font-weight: var(--font-weight-regular); + margin-left: 0; + text-decoration: none !important; + text-transform: uppercase; + vertical-align: middle; + + span { + font-weight: var(--font-weight-regular); + vertical-align: middle; + } + + &:hover { + color: var(--brand-light); + } + + svg { + margin-left: 0.5rem; + vertical-align: middle; + } + } +} diff --git a/components/Article/EditLink/index.stories.tsx b/components/Article/EditLink/index.stories.tsx new file mode 100644 index 0000000000000..6ed6c3c9fa688 --- /dev/null +++ b/components/Article/EditLink/index.stories.tsx @@ -0,0 +1,11 @@ +import EditLink from './index'; +import type { Meta as MetaObj, StoryObj } from '@storybook/react'; + +type Story = StoryObj; +type Meta = MetaObj; + +export const Default: Story = { + args: { relativePath: 'get-involved/contribute.md' }, +}; + +export default { component: EditLink } as Meta; diff --git a/components/Article/EditLink/index.tsx b/components/Article/EditLink/index.tsx new file mode 100644 index 0000000000000..0cae2eccd77ce --- /dev/null +++ b/components/Article/EditLink/index.tsx @@ -0,0 +1,76 @@ +import { FaPencilAlt } from 'react-icons/fa'; +import { FormattedMessage } from 'react-intl'; +import styles from './index.module.scss'; +import { useLocale } from './../../../hooks/useLocale'; +import type { FC } from 'react'; + +type EditLinkProps = { + absolutePath?: string; + relativePath?: string; + editPath?: string; +}; + +// TODO(HinataKah0): Change branch from major/website-redesign to main + +const baseEditURL = + 'https://github.com/nodejs/nodejs.org/edit/major/website-redesign'; + +const translationReadmeURL = + 'https://github.com/nodejs/nodejs.org/blob/major/website-redesign/TRANSLATION.md'; + +const translationKeyPrefix = 'components.article.editLink.title'; + +type EditLinkParams = { + translationKey: string; + href: string; +}; + +const getEditLinkParams = ( + { absolutePath, relativePath, editPath }: EditLinkProps, + lang: string +): EditLinkParams => { + if (lang === 'en') { + // Initial content development is done on GitHub in English + return { + translationKey: `${translationKeyPrefix}.edit`, + href: + absolutePath || + (relativePath + ? `${baseEditURL}/pages/en/${relativePath}` + : `${baseEditURL}/${editPath}`), + }; + } + + return { + translationKey: `${translationKeyPrefix}.translate`, + href: translationReadmeURL, + }; +}; + +const EditLink: FC = ({ + absolutePath, + relativePath, + editPath, +}) => { + const { currentLocale } = useLocale(); + + if (!relativePath && !editPath && !absolutePath) { + return null; + } + + const editLinkParams = getEditLinkParams( + { absolutePath, relativePath, editPath }, + currentLocale.code + ); + + return ( + + ); +}; + +export default EditLink; diff --git a/components/Article/InlineCode/__snapshots__/index.stories.tsx.snap b/components/Article/InlineCode/__snapshots__/index.stories.tsx.snap new file mode 100644 index 0000000000000..793021fb1421b --- /dev/null +++ b/components/Article/InlineCode/__snapshots__/index.stories.tsx.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Article/InlineCode Default smoke-test 1`] = ` + + + const a = 1; + + +`; diff --git a/components/Article/InlineCode/index.module.scss b/components/Article/InlineCode/index.module.scss new file mode 100644 index 0000000000000..dbecf56e3bc80 --- /dev/null +++ b/components/Article/InlineCode/index.module.scss @@ -0,0 +1,36 @@ +@use 'styles/mixins/components'; + +.code { + @include components.code; + + font-weight: var(--font-weight-light); + padding: 0 6px; + + white-space: break-spaces; +} + +[data-theme='light'] { + .code { + background-color: var(--black2); + color: var(--black9); + } + + a .code { + background-color: transparent; + color: var(--color-text-accent); + padding: 0; + } +} + +[data-theme='dark'] { + .code { + background-color: var(--black9); + color: var(--black2); + } + + a .code { + background-color: transparent; + color: var(--color-text-accent); + padding: 0; + } +} diff --git a/components/Article/InlineCode/index.stories.tsx b/components/Article/InlineCode/index.stories.tsx new file mode 100644 index 0000000000000..37d8503e65a52 --- /dev/null +++ b/components/Article/InlineCode/index.stories.tsx @@ -0,0 +1,20 @@ +import InlineCode from './index'; +import type { FC } from 'react'; +import type { Meta as MetaObj, StoryObj } from '@storybook/react'; + +type DecoratedInlineCodeProps = { code: string }; + +const DecoratedInlineCode: FC = ({ code }) => ( + + {code} + +); + +type Story = StoryObj; +type Meta = MetaObj; + +const code = 'const a = 1;'; + +export const Default: Story = { args: { code } }; + +export default { component: DecoratedInlineCode } as Meta; diff --git a/components/Article/InlineCode/index.tsx b/components/Article/InlineCode/index.tsx new file mode 100644 index 0000000000000..a3f79197169a9 --- /dev/null +++ b/components/Article/InlineCode/index.tsx @@ -0,0 +1,8 @@ +import styles from './index.module.scss'; +import type { FC, PropsWithChildren } from 'react'; + +const InlineCode: FC = ({ children }) => ( + {children} +); + +export default InlineCode; diff --git a/components/Blog/BlogCard/__snapshots__/index.stories.tsx.snap b/components/Blog/BlogCard/__snapshots__/index.stories.tsx.snap new file mode 100644 index 0000000000000..edd7adfa14ace --- /dev/null +++ b/components/Blog/BlogCard/__snapshots__/index.stories.tsx.snap @@ -0,0 +1,30 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Blog/BlogCard Default smoke-test 1`] = ` +
+ +
+

+ 21 April 2023 +

+

+ by + + Bat Man + +

+
+
+`; diff --git a/components/Blog/BlogCard/index.module.scss b/components/Blog/BlogCard/index.module.scss new file mode 100644 index 0000000000000..c16ec8aeabb9f --- /dev/null +++ b/components/Blog/BlogCard/index.module.scss @@ -0,0 +1,72 @@ +.blogCard { + display: flex; + flex-direction: column; + padding: var(--space-32) var(--space-24) var(--space-32) 0; + + @media (max-width: 900px) { + padding: var(--space-12) var(--space-24); + } + + .title { + background-color: var(--color-blog-card-background); + border-radius: 5px; + display: flex; + flex: 1 1 0px; + flex-direction: column; + padding: 1rem 1.5rem; + + a { + color: var(--color-text-accent); + font-size: 2em; + margin-bottom: 10px; + text-decoration: none; + + &:hover { + text-decoration: underline; + } + } + } + + .metadata { + align-self: flex-start; + background-color: var(--color-dropdown-hover); + border-radius: 1rem; + display: flex; + margin-top: auto; + padding: 0.25rem; + + span, + a { + color: var(--color-text-high-contrast); + font-size: var(--font-size-body3); + padding: 0.125rem 0.5rem; + + &.category { + background-color: var(--color-dropdown-background); + border-radius: 1rem; + margin: 0; + } + } + } + + .content { + justify-self: flex-end; + + h4 { + margin: 0; + margin-top: 7px; + opacity: 0.7; + } + + p { + margin: 7px 0; + opacity: 0.8; + + li { + display: inline; + list-style: none; + margin: 0 3px; + } + } + } +} diff --git a/components/Blog/BlogCard/index.stories.tsx b/components/Blog/BlogCard/index.stories.tsx new file mode 100644 index 0000000000000..5a7f848ab6eee --- /dev/null +++ b/components/Blog/BlogCard/index.stories.tsx @@ -0,0 +1,18 @@ +import BlockQuote from './index'; +import type { Meta as MetaObj, StoryObj } from '@storybook/react'; + +type Story = StoryObj; +type Meta = MetaObj; + +export const Default: Story = { + args: { + author: 'Bat Man', + category: 'category-mock', + date: '2023-04-21 23:40:56.77', + slug: '/blog/category-mock/sample-blog', + title: 'Sample Test Blog', + readingTime: '1 min read', + }, +}; + +export default { component: BlockQuote } as Meta; diff --git a/components/Blog/BlogCard/index.tsx b/components/Blog/BlogCard/index.tsx new file mode 100644 index 0000000000000..992e16145a086 --- /dev/null +++ b/components/Blog/BlogCard/index.tsx @@ -0,0 +1,50 @@ +import { FormattedDate, FormattedMessage } from 'react-intl'; +import styles from './index.module.scss'; +import LocalizedLink from '../../LocalizedLink'; +import navigation from '../../../navigation.json'; +import type { BlogPost } from '../../../types'; +import type { FC } from 'react'; + +const getBlogCategoryUrl = (category: string): string => + `${navigation.blog.link}/${category}/`; + +type BlogCardProps = BlogPost & { readingTime: string }; + +const BlogCard: FC = ({ + title, + author, + date, + category, + readingTime, + slug, +}) => ( +
+
+ {title} +
+ {category && ( + + {category} + + )} + {readingTime} +
+
+
+

+ +

+ {author && ( +

+ {' '} + {author} +

+ )} +
+
+); + +export default BlogCard; diff --git a/components/Common/ActiveLocalizedLink/__tests__/index.test.tsx b/components/Common/ActiveLocalizedLink/__tests__/index.test.tsx new file mode 100644 index 0000000000000..f00665c95cad7 --- /dev/null +++ b/components/Common/ActiveLocalizedLink/__tests__/index.test.tsx @@ -0,0 +1,62 @@ +import { IntlProvider } from 'react-intl'; +import { render, screen } from '@testing-library/react'; +import ActiveLocalizedLink from '..'; + +jest.mock('next/router', () => ({ + useRouter() { + return { + isReady: true, + asPath: '/link', + }; + }, +})); + +describe('ActiveLocalizedLink', () => { + it('renders as localized link', () => { + render( + {}}> + + Link + + + ); + + expect(screen.getByText('Link')).toHaveAttribute('href', '/en/link'); + }); + + it('ignores active class when href not matches location.href', () => { + render( + {}}> + + Link + + + ); + + expect(screen.getByText('Link')).toHaveAttribute('class', 'link'); + }); + + it('sets active class when href matches location.href', () => { + render( + {}}> + + Link + + + ); + + expect(screen.getByText('Link')).toHaveAttribute('class', 'link active'); + }); +}); diff --git a/components/Common/ActiveLocalizedLink/index.tsx b/components/Common/ActiveLocalizedLink/index.tsx new file mode 100644 index 0000000000000..55be2b7488e65 --- /dev/null +++ b/components/Common/ActiveLocalizedLink/index.tsx @@ -0,0 +1,59 @@ +import { useRouter } from 'next/router'; +import { useState, useEffect, type FC } from 'react'; +import classNames from 'classnames'; +import LocalizedLink from '../../LocalizedLink'; +import type Link from 'next/link'; +import type { ComponentProps } from 'react'; + +type ActiveLocalizedLinkProps = ComponentProps & { + activeClassName: string; +}; + +const ActiveLocalizedLink: FC = ({ + children, + activeClassName, + className, + ...props +}) => { + const { asPath, isReady } = useRouter(); + + const [computedClassName, setComputedClassName] = useState(className); + + useEffect(() => { + // Check if the router fields are updated client-side + if (isReady) { + const currentHref = (props.as || props.href).toString(); + + // Dynamic route will be matched via props.as + // Static route will be matched via props.href + const linkURL = new URL(currentHref, location.href); + + // Using URL().pathname to get rid of query and hash + const currentPathName = new URL(asPath, location.href).pathname; + + const newClassName = classNames(className, { + [activeClassName]: linkURL.pathname === currentPathName, + }); + + if (newClassName !== computedClassName) { + setComputedClassName(newClassName); + } + } + }, [ + asPath, + isReady, + props.as, + props.href, + activeClassName, + className, + computedClassName, + ]); + + return ( + + {children} + + ); +}; + +export default ActiveLocalizedLink; diff --git a/components/Common/AnimatedPlaceholder/__snapshots__/index.stories.tsx.snap b/components/Common/AnimatedPlaceholder/__snapshots__/index.stories.tsx.snap new file mode 100644 index 0000000000000..5ae238eedf6d4 --- /dev/null +++ b/components/Common/AnimatedPlaceholder/__snapshots__/index.stories.tsx.snap @@ -0,0 +1,21 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Common/AnimatedPlaceholder Default smoke-test 1`] = ` +
+
+
+
+
+
+
+
+
+
+`; + +exports[`Common/AnimatedPlaceholder WithLoaderSkeleton smoke-test 1`] = ` +
+
+
+
+`; diff --git a/components/Common/AnimatedPlaceholder/index.module.scss b/components/Common/AnimatedPlaceholder/index.module.scss new file mode 100644 index 0000000000000..43182e38ab6b1 --- /dev/null +++ b/components/Common/AnimatedPlaceholder/index.module.scss @@ -0,0 +1,47 @@ +@keyframes placeHolderShimmer { + 0% { + background-position: -468px 0; + } + 100% { + background-position: 468px 0; + } +} + +.animatedBackground, +%animated-background { + animation-duration: 1.25s; + animation-fill-mode: forwards; + animation-iteration-count: infinite; + animation-name: placeHolderShimmer; + animation-timing-function: linear; + background: #f6f6f6; + background: linear-gradient(to right, #f6f6f6 8%, #f0f0f0 18%, #f6f6f6 33%); + background-size: 800px 104px; + height: 96px; + position: relative; +} + +.placeholder { + display: flex; + width: 100%; + + &Image { + @extend %animated-background; + + height: 40px; + margin-right: 5px; + min-width: 40px; + } + + &Text { + width: 100%; + } + + &TextLine { + @extend %animated-background; + + height: 10px; + margin: 4px 0; + width: 100%; + } +} diff --git a/components/Common/AnimatedPlaceholder/index.stories.tsx b/components/Common/AnimatedPlaceholder/index.stories.tsx new file mode 100644 index 0000000000000..1fe0db1e901d0 --- /dev/null +++ b/components/Common/AnimatedPlaceholder/index.stories.tsx @@ -0,0 +1,15 @@ +import AnimatedPlaceholder from './index'; +import type { Meta as MetaObj, StoryObj } from '@storybook/react'; + +type Story = StoryObj; +type Meta = MetaObj; + +export const Default: Story = {}; + +export const WithLoaderSkeleton: Story = { + args: { + children:
, + }, +}; + +export default { component: AnimatedPlaceholder } as Meta; diff --git a/components/Common/AnimatedPlaceholder/index.tsx b/components/Common/AnimatedPlaceholder/index.tsx new file mode 100644 index 0000000000000..b7f785270e20b --- /dev/null +++ b/components/Common/AnimatedPlaceholder/index.tsx @@ -0,0 +1,31 @@ +import styles from './index.module.scss'; +import type { FC, ReactNode } from 'react'; + +type AnimatedPlaceholderProps = { + children?: ReactNode; + width?: number; + height?: number; +}; + +const AnimatedPlaceholder: FC = ({ + children, + width, + height, +}) => ( +
+ {children || ( + <> +
+
+
+
+
+ + )} +
+); + +export default AnimatedPlaceholder; diff --git a/components/Common/Banner/__snapshots__/index.stories.tsx.snap b/components/Common/Banner/__snapshots__/index.stories.tsx.snap new file mode 100644 index 0000000000000..2f0c316e29cd1 --- /dev/null +++ b/components/Common/Banner/__snapshots__/index.stories.tsx.snap @@ -0,0 +1,41 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Common/Banner WithHTML smoke-test 1`] = ` + +`; + +exports[`Common/Banner WithHTMLImage smoke-test 1`] = ` + +`; + +exports[`Common/Banner WithText smoke-test 1`] = ` +
+

+ + Read More + + Banner Text +

+
+`; diff --git a/components/Common/Banner/__tests__/index.test.tsx b/components/Common/Banner/__tests__/index.test.tsx new file mode 100644 index 0000000000000..244145754b313 --- /dev/null +++ b/components/Common/Banner/__tests__/index.test.tsx @@ -0,0 +1,139 @@ +import { render, screen } from '@testing-library/react'; +import { IntlProvider } from 'react-intl'; +import Banner from '../index'; +import type { WebsiteBanner } from '../../../../types'; + +const bannersIndex: WebsiteBanner = { + endDate: '', + link: 'test/banner/link', + text: 'Test banner text', + startDate: '', +}; + +describe('Tests for Header component', () => { + it('renders when today between startDate and endDate', () => { + const beforeToday = new Date(); + beforeToday.setDate(beforeToday.getDate() - 1); + const afterToday = new Date(); + afterToday.setDate(afterToday.getDate() + 1); + + bannersIndex.startDate = beforeToday.toISOString(); + bannersIndex.endDate = afterToday.toISOString(); + + render( + {}}> + + + ); + + const bannerText = screen.getByText(bannersIndex.text || ''); + expect(bannerText).toBeInTheDocument(); + }); + + it('does not render when today before startDate', () => { + const beforeToday = new Date(); + beforeToday.setDate(beforeToday.getDate() + 1); + const afterToday = new Date(); + afterToday.setDate(afterToday.getDate() + 2); + + bannersIndex.startDate = beforeToday.toISOString(); + bannersIndex.endDate = afterToday.toISOString(); + + render( + {}}> + + + ); + + const bannerText = screen.queryByText(bannersIndex.text || ''); + expect(bannerText).not.toBeInTheDocument(); + }); + + it('does not render when today after endDate', () => { + const beforeToday = new Date(); + beforeToday.setDate(beforeToday.getDate() - 2); + const afterToday = new Date(); + afterToday.setDate(afterToday.getDate() - 1); + + bannersIndex.startDate = beforeToday.toISOString(); + bannersIndex.endDate = afterToday.toISOString(); + + render( + {}}> + + + ); + + const bannerText = screen.queryByText(bannersIndex.text || ''); + expect(bannerText).not.toBeInTheDocument(); + }); + + it('should use the supplied relative link', () => { + const beforeToday = new Date(); + beforeToday.setDate(beforeToday.getDate() - 1); + const afterToday = new Date(); + afterToday.setDate(afterToday.getDate() + 1); + + bannersIndex.startDate = beforeToday.toISOString(); + bannersIndex.endDate = afterToday.toISOString(); + bannersIndex.link = 'foo/bar'; + + render( + {}}> + + + ); + + const bannerText = screen.getByText(bannersIndex.text || ''); + expect(bannerText).toBeInTheDocument(); + + const bannerLink = bannerText.innerHTML; + expect(bannerLink).toMatch('http://nodejs.org/foo/bar'); + }); + + it('should use the supplied absolute link', () => { + const beforeToday = new Date(); + beforeToday.setDate(beforeToday.getDate() - 1); + const afterToday = new Date(); + afterToday.setDate(afterToday.getDate() + 1); + + bannersIndex.startDate = beforeToday.toISOString(); + bannersIndex.endDate = afterToday.toISOString(); + bannersIndex.link = 'https://nodejs.org/en/an-absolute-content'; + + render( + {}}> + + + ); + + const bannerText = screen.getByText(bannersIndex.text || ''); + expect(bannerText).toBeInTheDocument(); + + const bannerLink = bannerText.innerHTML; + expect(bannerLink).toMatch('https://nodejs.org/en/an-absolute-content'); + }); + + it('should display html content correctly', () => { + const beforeToday = new Date(); + beforeToday.setDate(beforeToday.getDate() - 1); + const afterToday = new Date(); + afterToday.setDate(afterToday.getDate() + 1); + + bannersIndex.startDate = beforeToday.toISOString(); + bannersIndex.endDate = afterToday.toISOString(); + bannersIndex.link = 'https://nodejs.org/en/an-absolute-content'; + bannersIndex.text = undefined; + bannersIndex.html = + 'Node.js'; + + render( + {}}> + + + ); + + const bannerImage = screen.getByTestId('test-image'); + expect(bannerImage).toBeInTheDocument(); + }); +}); diff --git a/components/Common/Banner/index.module.scss b/components/Common/Banner/index.module.scss new file mode 100644 index 0000000000000..98be8f6b45dd7 --- /dev/null +++ b/components/Common/Banner/index.module.scss @@ -0,0 +1,70 @@ +.banner { + color: var(--color-text-primary); + margin: 0 auto; + max-width: 90vw; + padding-top: var(--space-08); + position: relative; + + &, + p { + font-size: var(--font-size-body1); + font-weight: var(--font-weight-bold); + } + + a { + align-items: center; + color: var(--color-text-primary); + display: flex; + flex-direction: column; + text-decoration: none; + + &:hover { + text-decoration: underline; + } + + img { + border-radius: 5px; + max-width: 100%; + object-fit: cover; + } + + &.bannerBtn { + background: var(--purple5); + border: 1px solid transparent; + border-radius: 5.6rem; + color: var(--color-fill-top-nav); + font-family: var(--sans); + font-size: 1rem; + font-style: normal; + font-weight: var(--font-weight-semibold); + line-height: var(--line-height-subheading); + margin-right: var(--space-32); + padding: 0 var(--space-16); + position: relative; + text-decoration: none; + white-space: nowrap; + + &:hover { + background-color: var(--color-text-primary); + cursor: pointer; + } + } + } + + p { + align-items: center; + background-color: var(--color-fill-banner); + border-radius: 5px; + display: flex; + flex-direction: row; + margin: 0; + padding: var(--space-12); + text-align: center; + + a { + &:hover { + text-decoration: underline; + } + } + } +} diff --git a/components/Common/Banner/index.stories.tsx b/components/Common/Banner/index.stories.tsx new file mode 100644 index 0000000000000..1688438477a92 --- /dev/null +++ b/components/Common/Banner/index.stories.tsx @@ -0,0 +1,51 @@ +import Banner from './index'; +import type { Meta as MetaObj, StoryObj } from '@storybook/react'; + +type Story = StoryObj; +type Meta = MetaObj; + +const addDaysToDate = (numDays: number, date: Date) => { + const newDate = new Date(date); + newDate.setDate(date.getDate() + numDays); + return newDate; +}; + +// Create mock start and end dates as Banner Component renders +// only if end date is on or after today's date +const startDate = new Date(); +const endDate = addDaysToDate(3, startDate); + +export const WithText: Story = { + args: { + bannersIndex: { + startDate: startDate.toISOString(), + endDate: endDate.toISOString(), + text: 'Banner Text', + link: 'https://nodejs.org/en/', + }, + }, +}; + +export const WithHTML: Story = { + args: { + bannersIndex: { + startDate: startDate.toISOString(), + endDate: endDate.toISOString(), + html: '

Banner HTML

', + link: 'https://nodejs.org/en/', + }, + }, +}; + +export const WithHTMLImage: Story = { + args: { + bannersIndex: { + startDate: startDate.toISOString(), + endDate: endDate.toISOString(), + html: 'Banner Image', + link: 'https://nodejs.org/en/', + }, + }, +}; + +export default { component: Banner } as Meta; diff --git a/components/Common/Banner/index.tsx b/components/Common/Banner/index.tsx new file mode 100644 index 0000000000000..1bd093881bff7 --- /dev/null +++ b/components/Common/Banner/index.tsx @@ -0,0 +1,78 @@ +import { useMemo } from 'react'; +import { useIntl } from 'react-intl'; +import styles from './index.module.scss'; +import { dateIsBetween } from '../../../util/dateIsBetween'; +import { isAbsoluteUrl } from '../../../util/isAbsoluteUrl'; +import type { FC } from 'react'; +import type { WebsiteBanner } from '../../../types'; + +const useTextContent = ({ text, link }: WebsiteBanner, bannerBtnText: string) => + useMemo(() => { + if (text) { + return ( +

+ + {bannerBtnText || 'Read More'} + + {text} +

+ ); + } + + return null; + }, [text, link, bannerBtnText]); + +const useHtmlContent = ({ html, link }: WebsiteBanner) => + useMemo(() => { + if (html) { + return ( + + ); + } + + return null; + }, [html, link]); + +type BannerProps = { bannersIndex: WebsiteBanner }; + +const Banner: FC = ({ bannersIndex }) => { + const { formatMessage } = useIntl(); + + const showBanner = dateIsBetween( + bannersIndex.startDate, + bannersIndex.endDate + ); + + const link = !isAbsoluteUrl(bannersIndex.link) + ? `http://nodejs.org/${bannersIndex.link}` + : bannersIndex.link; + + const textContent = useTextContent( + { ...bannersIndex, link }, + formatMessage({ id: 'components.common.banner.button.text' }) + ); + + const htmlContent = useHtmlContent({ ...bannersIndex, link }); + + if (showBanner) { + return ( +
+ {bannersIndex.text ? textContent : htmlContent} +
+ ); + } + + return null; +}; + +export default Banner; diff --git a/components/Common/DarkModeToggle/__snapshots__/index.stories.tsx.snap b/components/Common/DarkModeToggle/__snapshots__/index.stories.tsx.snap new file mode 100644 index 0000000000000..1626b2d0c883b --- /dev/null +++ b/components/Common/DarkModeToggle/__snapshots__/index.stories.tsx.snap @@ -0,0 +1,39 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Common/DarkModeToggle Default smoke-test 1`] = ` + +`; diff --git a/components/Common/DarkModeToggle/__tests__/index.test.tsx b/components/Common/DarkModeToggle/__tests__/index.test.tsx new file mode 100644 index 0000000000000..4fb5097264eb3 --- /dev/null +++ b/components/Common/DarkModeToggle/__tests__/index.test.tsx @@ -0,0 +1,45 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { IntlProvider } from 'react-intl'; +import DarkModeToggle from '../index'; + +let mockCurrentTheme = 'light'; + +const mockToggleTheme = jest.fn().mockImplementation(() => { + mockCurrentTheme = mockCurrentTheme === 'dark' ? 'light' : 'dark'; +}); + +// Mock dark mode module for controlling dark mode HOC behaviour +jest.mock('next-themes', () => ({ + useTheme: () => { + return { theme: mockCurrentTheme, setTheme: mockToggleTheme }; + }, +})); + +describe('DarkModeToggle Component', () => { + it('switches dark theme to light theme', async () => { + const user = userEvent.setup(); + mockCurrentTheme = 'dark'; + render( + {}}> + + + ); + const toggle = screen.getByRole('button'); + await user.click(toggle); + expect(mockCurrentTheme).toBe('light'); + }); + + it('switches light theme to dark theme', async () => { + const user = userEvent.setup(); + mockCurrentTheme = 'light'; + render( + {}}> + + + ); + const toggle = screen.getByRole('button'); + await user.click(toggle); + expect(mockCurrentTheme).toBe('dark'); + }); +}); diff --git a/components/Common/DarkModeToggle/index.module.scss b/components/Common/DarkModeToggle/index.module.scss new file mode 100644 index 0000000000000..62ca5ac9f6aff --- /dev/null +++ b/components/Common/DarkModeToggle/index.module.scss @@ -0,0 +1,12 @@ +.darkModeToggle { + background: none; + border: none; + color: var(--color-text-accent); + cursor: pointer; + line-height: 0; + padding: 0; + + svg { + font-size: 2rem; + } +} diff --git a/components/Common/DarkModeToggle/index.stories.tsx b/components/Common/DarkModeToggle/index.stories.tsx new file mode 100644 index 0000000000000..1cf630c82b7df --- /dev/null +++ b/components/Common/DarkModeToggle/index.stories.tsx @@ -0,0 +1,9 @@ +import DarkModeToggle from './index'; +import type { Meta as MetaObj, StoryObj } from '@storybook/react'; + +type Story = StoryObj; +type Meta = MetaObj; + +export const Default: Story = {}; + +export default { component: DarkModeToggle } as Meta; diff --git a/components/Common/DarkModeToggle/index.tsx b/components/Common/DarkModeToggle/index.tsx new file mode 100644 index 0000000000000..71d62f9e46829 --- /dev/null +++ b/components/Common/DarkModeToggle/index.tsx @@ -0,0 +1,40 @@ +import { useTheme } from 'next-themes'; +import { useIntl } from 'react-intl'; +import { MdLightMode, MdNightlight } from 'react-icons/md'; +import styles from './index.module.scss'; + +const DarkModeToggle = () => { + const { theme, setTheme } = useTheme(); + + const intl = useIntl(); + + const isDark = theme === 'dark'; + + const toggleTheme = (isKeyPress?: boolean) => { + if (isKeyPress) { + return; + } + + setTheme(isDark ? 'light' : 'dark'); + }; + + const ariaLabelText = intl.formatMessage({ + id: 'components.header.buttons.toggleDarkMode', + }); + + return ( + + ); +}; + +export default DarkModeToggle; diff --git a/components/Common/Dropdown/__snapshots__/index.stories.tsx.snap b/components/Common/Dropdown/__snapshots__/index.stories.tsx.snap new file mode 100644 index 0000000000000..d6686d4de3039 --- /dev/null +++ b/components/Common/Dropdown/__snapshots__/index.stories.tsx.snap @@ -0,0 +1,76 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Common/Dropdown Default smoke-test 1`] = ` +
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+`; diff --git a/components/Common/Dropdown/__tests__/index.test.tsx b/components/Common/Dropdown/__tests__/index.test.tsx new file mode 100644 index 0000000000000..ebf0fe5069cd4 --- /dev/null +++ b/components/Common/Dropdown/__tests__/index.test.tsx @@ -0,0 +1,64 @@ +import { fireEvent, render, screen } from '@testing-library/react'; +import Dropdown from '..'; +import type { DropdownItem } from '../../../../types'; + +describe('Dropdown component', () => { + const items: DropdownItem[] = [ + { label: 'item1', title: 'Item 1', active: false, onClick: jest.fn() }, + { label: 'item2', title: 'Item 2', active: true, onClick: jest.fn() }, + { label: 'item3', title: 'Item 3', active: false, onClick: jest.fn() }, + ]; + + it('should render the items and apply active styles', () => { + render(); + + items.forEach(item => { + const button = screen.getByText(item.title); + expect(button).toBeInTheDocument(); + + if (item.active) { + expect(button).toHaveStyle('font-weight: bold'); + } else { + expect(button).not.toHaveStyle('font-weight: bold'); + } + }); + }); + + it('should call the onClick function when an item is clicked', () => { + render(); + const button = screen.getByText(items[2].title); + fireEvent.click(button); + expect(items[2].onClick).toHaveBeenCalledTimes(1); + }); + + it('should call the onClick function when Enter or Space is pressed', () => { + render(); + const button = screen.getByText(items[1].title); + fireEvent.keyDown(button, { key: 'Enter', code: 'Enter' }); + fireEvent.keyDown(button, { key: ' ', code: 'Space' }); + expect(items[1].onClick).toHaveBeenCalledTimes(2); + }); + + it('should not render the items when shouldShow prop is false', () => { + render(); + items.forEach(item => { + const button = screen.queryByText(item.title); + expect(button).not.toBeVisible(); + }); + }); + + it('should apply styles passed in the styles prop', () => { + const customStyles = { + backgroundColor: 'green', + padding: '10px', + borderRadius: '5px', + }; + + render(); + + const dropdownList = screen.getByRole('list'); + expect(dropdownList).toHaveStyle('background-color: green'); + expect(dropdownList).toHaveStyle('padding: 10px'); + expect(dropdownList).toHaveStyle('border-radius: 5px'); + }); +}); diff --git a/components/Common/Dropdown/index.module.scss b/components/Common/Dropdown/index.module.scss new file mode 100644 index 0000000000000..376988645b40b --- /dev/null +++ b/components/Common/Dropdown/index.module.scss @@ -0,0 +1,30 @@ +.dropdownList { + background-color: var(--color-dropdown-background); + border-radius: 5px; + height: fit-content; + list-style-type: none; + max-height: 200px; + min-width: 150px; + overflow-y: auto; + padding: 0; + position: absolute; + width: fit-content; + + > li { + > button { + background: none; + border: none; + + color: var(--color-text-primary); + cursor: pointer; + font-size: var(--font-size-body1); + padding: var(--space-12); + text-align: center; + width: 100%; + + &:hover { + background-color: var(--color-dropdown-hover); + } + } + } +} diff --git a/components/Common/Dropdown/index.stories.tsx b/components/Common/Dropdown/index.stories.tsx new file mode 100644 index 0000000000000..61fdf62aa8cf1 --- /dev/null +++ b/components/Common/Dropdown/index.stories.tsx @@ -0,0 +1,23 @@ +import Dropdown from './index'; +import type { Meta as MetaObj, StoryObj } from '@storybook/react'; + +type Story = StoryObj; +type Meta = MetaObj; + +const items = [...Array(10).keys()].map(item => ({ + title: `Item ${item + 1}`, + label: `item-${item + 1}`, + active: false, + onClick: () => {}, +})); + +items[2].active = true; + +export const Default: Story = { + args: { + items: items, + shouldShow: true, + }, +}; + +export default { component: Dropdown } as Meta; diff --git a/components/Common/Dropdown/index.tsx b/components/Common/Dropdown/index.tsx new file mode 100644 index 0000000000000..acc44d1da5a49 --- /dev/null +++ b/components/Common/Dropdown/index.tsx @@ -0,0 +1,51 @@ +import styles from './index.module.scss'; +import type { DropdownItem } from '../../../types'; +import type { CSSProperties, FC, KeyboardEvent } from 'react'; + +type DropdownProps = { + items: Array; + shouldShow: boolean; + styles: CSSProperties; +}; + +const Dropdown: FC = ({ + items, + shouldShow, + styles: extraStyles, +}) => { + const mappedElements = items.map(item => { + const extraStyles = { fontWeight: item.active ? 'bold' : 'normal' }; + + const handleKeyPress = (e: KeyboardEvent) => { + if (e.key === 'Enter' || e.key === ' ') { + item.onClick(); + } + }; + + return ( +
  • + +
  • + ); + }); + + const dropdownStyles = { + display: shouldShow ? 'block' : 'none', + ...extraStyles, + }; + + return ( +
      + {mappedElements} +
    + ); +}; + +export default Dropdown; diff --git a/components/Common/LanguageSelector/__snapshots__/index.stories.tsx.snap b/components/Common/LanguageSelector/__snapshots__/index.stories.tsx.snap new file mode 100644 index 0000000000000..79de68502e1fe --- /dev/null +++ b/components/Common/LanguageSelector/__snapshots__/index.stories.tsx.snap @@ -0,0 +1,163 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Common/LanguageSelector Default smoke-test 1`] = ` +
    +
    + +
      +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    +
    +
    +`; diff --git a/components/Common/LanguageSelector/__tests__/index.test.tsx b/components/Common/LanguageSelector/__tests__/index.test.tsx new file mode 100644 index 0000000000000..8fbdb2c963a42 --- /dev/null +++ b/components/Common/LanguageSelector/__tests__/index.test.tsx @@ -0,0 +1,43 @@ +import { fireEvent, render, screen } from '@testing-library/react'; +import { IntlProvider } from 'react-intl'; +import LanguageSelector from '..'; + +jest.mock('../../../../hooks/useLocale', () => ({ + useLocale: () => ({ + availableLocales: [ + { code: 'en', name: 'English', localName: 'English' }, + { code: 'es', name: 'Spanish', localName: 'Español' }, + ], + currentLocale: { code: 'en', name: 'English', localName: 'English' }, + }), +})); + +describe('LanguageSelector', () => { + test('clicking the language switch button toggles the dropdown display', () => { + render( + {}}> + + + ); + const button = screen.getByRole('button'); + expect(screen.queryByText('English')).not.toBeVisible(); + fireEvent.click(button); + expect(screen.queryByText('English')).toBeVisible(); + fireEvent.click(button); + expect(screen.queryByText('English')).not.toBeVisible(); + }); + + test('renders the Dropdown component with correct style', () => { + render( + {}}> + + + ); + const button = screen.getByRole('button'); + fireEvent.click(button); + const dropdown = screen.getByRole('list'); + expect(dropdown).toHaveStyle( + 'position: absolute; top: 60%; right: 0; margin: 0;' + ); + }); +}); diff --git a/components/Common/LanguageSelector/index.module.scss b/components/Common/LanguageSelector/index.module.scss new file mode 100644 index 0000000000000..9f9c7583acabc --- /dev/null +++ b/components/Common/LanguageSelector/index.module.scss @@ -0,0 +1,17 @@ +.languageSwitch { + background: none; + border: none; + color: var(--color-text-accent); + cursor: pointer; + line-height: 0; + padding: 0; + + svg { + font-size: 2rem; + } +} + +.container { + display: inline; + position: relative; +} diff --git a/components/Common/LanguageSelector/index.stories.tsx b/components/Common/LanguageSelector/index.stories.tsx new file mode 100644 index 0000000000000..ff88a0f32e206 --- /dev/null +++ b/components/Common/LanguageSelector/index.stories.tsx @@ -0,0 +1,20 @@ +import LanguageSelector from './index'; +import type { Meta as MetaObj, StoryObj } from '@storybook/react'; + +type Story = StoryObj; +type Meta = MetaObj; + +export const Default: Story = {}; + +const containerStyles = { marginLeft: '200px' } as const; + +export default { + component: LanguageSelector, + decorators: [ + Story => ( +
    + +
    + ), + ], +} as Meta; diff --git a/components/Common/LanguageSelector/index.tsx b/components/Common/LanguageSelector/index.tsx new file mode 100644 index 0000000000000..4fbcfab57b3ce --- /dev/null +++ b/components/Common/LanguageSelector/index.tsx @@ -0,0 +1,64 @@ +import { useMemo, useState, useCallback } from 'react'; +import { MdOutlineTranslate } from 'react-icons/md'; +import { useIntl } from 'react-intl'; +import styles from './index.module.scss'; +import Dropdown from '../Dropdown'; +import { useLocale } from '../../../hooks/useLocale'; +import { useClickOutside } from '../../../hooks/useClickOutside'; + +const dropdownStyle = { + position: 'absolute', + top: '60%', + right: '0', + margin: 0, +} as const; + +const LanguageSelector = () => { + const [showDropdown, setShowDropdown] = useState(false); + + const dropdownHandler = useCallback(() => setShowDropdown(false), []); + const ref = useClickOutside(dropdownHandler); + + const { availableLocales, currentLocale } = useLocale(); + + const intl = useIntl(); + + const dropdownItems = useMemo( + () => + availableLocales.map(locale => ({ + title: locale.localName, + label: locale.name, + onClick: () => { + // TODO: "locale changing logic yet to be implemented" + }, + active: currentLocale.code === locale.code, + })), + [availableLocales, currentLocale] + ); + + const ariaLabelText = intl.formatMessage({ + id: 'components.common.languageSelector.button.title', + }); + + return ( +
    + + + +
    + ); +}; + +export default LanguageSelector; diff --git a/components/Common/SectionTitle/__snapshots__/index.stories.ts.snap b/components/Common/SectionTitle/__snapshots__/index.stories.ts.snap new file mode 100644 index 0000000000000..692a1314bc76a --- /dev/null +++ b/components/Common/SectionTitle/__snapshots__/index.stories.ts.snap @@ -0,0 +1,10 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Common/SectionTitle Default smoke-test 1`] = ` +
    + home / previous / + + current + +
    +`; diff --git a/components/Common/SectionTitle/__tests__/SectionTitle.test.tsx b/components/Common/SectionTitle/__tests__/SectionTitle.test.tsx new file mode 100644 index 0000000000000..ce6d27420c3b4 --- /dev/null +++ b/components/Common/SectionTitle/__tests__/SectionTitle.test.tsx @@ -0,0 +1,12 @@ +import { render, screen } from '@testing-library/react'; +import SectionTitle from '..'; + +describe('SectionTitle component', () => { + const mockData = ['home', 'previous', 'current']; + + it('last item should be active', () => { + render(); + const active = screen.getByText(mockData[mockData.length - 1]); + expect(active).toHaveClass('active'); + }); +}); diff --git a/components/Common/SectionTitle/index.module.scss b/components/Common/SectionTitle/index.module.scss new file mode 100644 index 0000000000000..018959b7515b0 --- /dev/null +++ b/components/Common/SectionTitle/index.module.scss @@ -0,0 +1,13 @@ +.sectionTitle, +%section-title { + color: var(--color-text-primary); + font-size: var(--font-size-overline); + font-weight: var(--font-weight-semibold); + letter-spacing: var(--space-02); + line-height: var(--line-height-overline); + text-transform: uppercase; + + .active { + color: var(--brand4); + } +} diff --git a/components/Common/SectionTitle/index.stories.ts b/components/Common/SectionTitle/index.stories.ts new file mode 100644 index 0000000000000..26f9599a3c006 --- /dev/null +++ b/components/Common/SectionTitle/index.stories.ts @@ -0,0 +1,13 @@ +import SectionTitle from './index'; +import type { Meta as MetaObj, StoryObj } from '@storybook/react'; + +type Story = StoryObj; +type Meta = MetaObj; + +export const Default: Story = { + args: { + path: ['home', 'previous', 'current'], + }, +}; + +export default { component: SectionTitle } as Meta; diff --git a/components/Common/SectionTitle/index.tsx b/components/Common/SectionTitle/index.tsx new file mode 100644 index 0000000000000..cdc9362f2ee79 --- /dev/null +++ b/components/Common/SectionTitle/index.tsx @@ -0,0 +1,24 @@ +import styles from './index.module.scss'; +import type { FC } from 'react'; + +type SectionTitleProps = { path: string[] }; + +const SectionTitle: FC = ({ path }) => ( +
    + {path.map((item, index) => { + const isLast = index === path.length - 1; + + if (isLast) { + return ( + + {item} + + ); + } + + return `${item} / `; + })} +
    +); + +export default SectionTitle; diff --git a/components/Common/ShellBox/__snapshots__/index.stories.tsx.snap b/components/Common/ShellBox/__snapshots__/index.stories.tsx.snap new file mode 100644 index 0000000000000..d30ecd0e2ac5f --- /dev/null +++ b/components/Common/ShellBox/__snapshots__/index.stories.tsx.snap @@ -0,0 +1,54 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Common/ShellBox Default smoke-test 1`] = ` +
    +  
    + + SHELL + + +
    + + echo hello world + +
    +`; + +exports[`Common/ShellBox WithTextToCopyJSX smoke-test 1`] = ` +
    +  
    + + SHELL + + +
    + + + + $ + + echo hello world + + +
    +`; + +exports[`Common/ShellBox WithoutTextToCopy smoke-test 1`] = ` +
    +  
    + + SHELL + + +
    + + echo hello world + +
    +`; diff --git a/components/Common/ShellBox/__tests__/index.test.tsx b/components/Common/ShellBox/__tests__/index.test.tsx new file mode 100644 index 0000000000000..65c7dd3089aab --- /dev/null +++ b/components/Common/ShellBox/__tests__/index.test.tsx @@ -0,0 +1,49 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { IntlProvider } from 'react-intl'; +import ShellBox from '../index'; + +const mockWriteText = jest.fn(); +const originalNavigator = { ...window.navigator }; + +describe('ShellBox', () => { + beforeEach(() => { + Object.defineProperty(window, 'navigator', { + value: { + clipboard: { + writeText: mockWriteText, + }, + }, + }); + }); + + afterEach(() => { + Object.defineProperty(window, 'navigator', { + value: originalNavigator, + }); + }); + + it('should call clipboard API with `test` once', async () => { + const user = userEvent.setup(); + const navigatorClipboardWriteTextSpy = jest + .fn() + .mockImplementation(() => Promise.resolve()); + + Object.defineProperty(window.navigator, 'clipboard', { + writable: true, + value: { + writeText: navigatorClipboardWriteTextSpy, + }, + }); + + render( + {}}> + test + + ); + const button = screen.getByRole('button'); + await user.click(button); + expect(navigatorClipboardWriteTextSpy).toHaveBeenCalledTimes(1); + expect(navigatorClipboardWriteTextSpy).toHaveBeenCalledWith('test'); + }); +}); diff --git a/components/Common/ShellBox/index.module.scss b/components/Common/ShellBox/index.module.scss new file mode 100644 index 0000000000000..f69a169efabc8 --- /dev/null +++ b/components/Common/ShellBox/index.module.scss @@ -0,0 +1,75 @@ +.shellBox { + background-color: var(--black10); + border-radius: 0.4rem; + box-sizing: border-box; + display: flex; + flex-direction: column; + font-family: var(--mono); + padding: 0 0 var(--space-48) var(--space-16); + position: relative; + + code { + color: var(--pink5); + font-family: inherit; + line-height: 30px; + overflow-x: hidden; + position: absolute; + top: 30px; + width: calc(100% - 20px); + + &:hover { + overflow-x: auto; + } + + &::-webkit-scrollbar { + height: 0.5em; + } + + &::-webkit-scrollbar, + &::-webkit-scrollbar-thumb { + border-radius: 4px; + overflow: visible; + } + + &::-webkit-scrollbar-thumb { + background: rgba(0, 0, 0, 0.2); + } + + > span.function { + color: var(--warning5); + } + } + + .top { + display: inherit; + flex-direction: row; + justify-content: space-between; + margin-bottom: var(--space-08); + + span, + button { + align-items: center; + display: inherit; + font-size: var(--font-size-code); + height: 23px; + justify-content: center; + width: 86px; + } + + span { + background-color: var(--black3); + border-radius: 0 0 0.3rem 0.3rem; + color: var(--black9); + margin-left: 1.6rem; + } + + button { + background-color: var(--brand); + border-radius: 0 0.3rem 0.3rem 0.3rem; + border-width: 0; + i { + padding: 0; + } + } + } +} diff --git a/components/Common/ShellBox/index.stories.tsx b/components/Common/ShellBox/index.stories.tsx new file mode 100644 index 0000000000000..ac8d5309cb8b2 --- /dev/null +++ b/components/Common/ShellBox/index.stories.tsx @@ -0,0 +1,31 @@ +import ShellBox from './index'; +import type { Meta as MetaObj, StoryObj } from '@storybook/react'; + +type Story = StoryObj; +type Meta = MetaObj; + +export const Default: Story = { + args: { + children: 'echo hello world', + textToCopy: 'echo hello world', + }, +}; + +export const WithoutTextToCopy: Story = { + args: { + children: 'echo hello world', + }, +}; + +export const WithTextToCopyJSX: Story = { + args: { + children: ( + + $echo hello world + + ), + textToCopy: 'echo hello world', + }, +}; + +export default { component: ShellBox } as Meta; diff --git a/components/Common/ShellBox/index.tsx b/components/Common/ShellBox/index.tsx new file mode 100644 index 0000000000000..273828e738ba1 --- /dev/null +++ b/components/Common/ShellBox/index.tsx @@ -0,0 +1,45 @@ +import { useRef } from 'react'; +import { FormattedMessage } from 'react-intl'; +import styles from './index.module.scss'; +import { useCopyToClipboard } from '../../../hooks/useCopyToClipboard'; +import type { FC, PropsWithChildren, MouseEvent, ReactNode } from 'react'; + +type ShellBoxProps = { + children: string | ReactNode; + textToCopy?: string; +}; + +const ShellBox: FC> = ({ + children, + textToCopy, +}: PropsWithChildren) => { + const [copied, copyText] = useCopyToClipboard(); + + const shellBoxRef = useRef(null); + + const handleCopyCode = async (event: MouseEvent) => { + event.preventDefault(); + // By default we want to use the textToCopy props but if it's absent + // we allow the user to copy by getting the inner HTML content of the Element + const _textToCopy = textToCopy || shellBoxRef.current?.innerHTML || ''; + + await copyText(_textToCopy.replace('$', '')); + }; + + return ( +
    +      
    + SHELL + +
    + {children} +
    + ); +}; + +export default ShellBox; diff --git a/components/Common/Time/index.tsx b/components/Common/Time/index.tsx new file mode 100644 index 0000000000000..1766b37cbef73 --- /dev/null +++ b/components/Common/Time/index.tsx @@ -0,0 +1,14 @@ +import type { FC } from 'react'; +import { FormattedDate } from 'react-intl'; + +type TimeProps = { date: string | Date; format: Intl.DateTimeFormatOptions }; + +export const Time: FC = ({ date, format }) => { + const dateObject = new Date(date); + + return ( + + ); +}; diff --git a/components/Common/index.ts b/components/Common/index.ts new file mode 100644 index 0000000000000..d49eb3b30e135 --- /dev/null +++ b/components/Common/index.ts @@ -0,0 +1 @@ +export { default as ShellBox } from './ShellBox'; diff --git a/components/Docs/NodeApiVersionLinks.tsx b/components/Docs/NodeApiVersionLinks.tsx new file mode 100644 index 0000000000000..2f28474c6a295 --- /dev/null +++ b/components/Docs/NodeApiVersionLinks.tsx @@ -0,0 +1,66 @@ +import { DOCS_URL } from '../../next.constants.mjs'; + +// Note.: This is a temporary Component used only until the transition to `nodejs/nodejs.dev` content is done +const NodeApiVersionLinks = () => ( +
    +); + +export default NodeApiVersionLinks; diff --git a/components/Downloads/DownloadList.tsx b/components/Downloads/DownloadList.tsx new file mode 100644 index 0000000000000..08efaade78d6e --- /dev/null +++ b/components/Downloads/DownloadList.tsx @@ -0,0 +1,33 @@ +import { FormattedMessage } from 'react-intl'; +import LocalizedLink from '../LocalizedLink'; +import { useNavigation } from '../../hooks/useNavigation'; +import type { NodeRelease } from '../../types'; +import type { FC } from 'react'; + +const DownloadList: FC = ({ versionWithPrefix }) => { + const { getSideNavigation } = useNavigation(); + + const [, ...downloadNavigation] = getSideNavigation('download', { + shaSums: { nodeVersion: versionWithPrefix }, + allDownloads: { nodeVersion: versionWithPrefix }, + }); + + return ( +
    +
      + {downloadNavigation.map((item, key) => ( +
    • + {item.text} + {item.key === 'shaSums' && ( + + + + )} +
    • + ))} +
    +
    + ); +}; + +export default DownloadList; diff --git a/components/Downloads/DownloadReleasesTable.tsx b/components/Downloads/DownloadReleasesTable.tsx new file mode 100644 index 0000000000000..d35fe9a94341b --- /dev/null +++ b/components/Downloads/DownloadReleasesTable.tsx @@ -0,0 +1,57 @@ +import { FormattedMessage } from 'react-intl'; +import { getNodejsChangelog } from '../../util/getNodeJsChangelog'; +import { getNodeApiLink } from '../../util/getNodeApiLink'; +import { useNodeReleases } from '../../hooks/useNodeReleases'; +import type { FC } from 'react'; + +const DownloadReleasesTable: FC = () => { + const { releases } = useNodeReleases(); + + return ( + + + + + + + + + + + + + + {releases.map(release => ( + + + + + + + + + + ))} + +
    VersionLTSDateV8npm + NODE_MODULE_VERSION[1] + +
    Node.js {release.version}{release.codename} + + {release.v8}{release.npm}{release.modules} + + + + + + + + + +
    + ); +}; + +export default DownloadReleasesTable; diff --git a/components/Downloads/DownloadToggle/__snapshots__/index.stories.tsx.snap b/components/Downloads/DownloadToggle/__snapshots__/index.stories.tsx.snap new file mode 100644 index 0000000000000..6c0fa0004329a --- /dev/null +++ b/components/Downloads/DownloadToggle/__snapshots__/index.stories.tsx.snap @@ -0,0 +1,53 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Downloads/DownloadToggle Current smoke-test 1`] = ` +
    +
    +
    + + +
    +
    +

    + With the latest features +

    +
    +`; + +exports[`Downloads/DownloadToggle Default smoke-test 1`] = ` +
    +
    +
    + + +
    +
    +

    + Recommended for most users +

    +
    +`; diff --git a/components/Downloads/DownloadToggle/__tests__/index.test.tsx b/components/Downloads/DownloadToggle/__tests__/index.test.tsx new file mode 100644 index 0000000000000..b50e37cd8adcf --- /dev/null +++ b/components/Downloads/DownloadToggle/__tests__/index.test.tsx @@ -0,0 +1,26 @@ +import userEvent from '@testing-library/user-event'; +import { render, screen } from '@testing-library/react'; +import { IntlProvider } from 'react-intl'; +import DownloadToggle from '..'; + +describe('DownloadToggle component', (): void => { + it('utilizes click handler correctly', async () => { + const mockHandler = jest.fn(); + + render( + {}}> + + + ); + + await userEvent.click( + screen.getByText('components.downloads.downloadToggle.lts') + ); + + await userEvent.click( + screen.getByText('components.downloads.downloadToggle.current') + ); + + expect(mockHandler.mock.calls.length).toBe(2); + }); +}); diff --git a/components/Downloads/DownloadToggle/index.module.scss b/components/Downloads/DownloadToggle/index.module.scss new file mode 100644 index 0000000000000..ebebd39ef1250 --- /dev/null +++ b/components/Downloads/DownloadToggle/index.module.scss @@ -0,0 +1,60 @@ +.downloadToggle { + align-items: center; + display: flex; + flex-direction: column; + justify-content: center; + margin-top: 40px; + + .selector { + display: flex; + justify-content: center; + } + + .switch { + border: none; + height: 42px; + padding: 0; + width: 236px; + } + + .description { + font-size: var(--font-size-body1); + line-height: var(--line-height-body1); + margin-top: 10px; + text-align: center; + } + + button { + background: var(--black0); + border: none; + border-radius: 32px; + box-shadow: + inset 1px -1px 1px rgba(0, 0, 0, 0.08), + inset 0px 1px 2px rgba(0, 0, 0, 0.08); + box-sizing: border-box; + color: var(--black7); + cursor: pointer; + display: inline-block; + font-size: var(--font-size-body2); + font-weight: var(--font-weight-bold); + height: 42px; + line-height: 40px; + line-height: var(--line-height-body2); + padding: 1px 6px; + position: relative; + width: 133px; + + &.current { + box-shadow: + inset 1px -1px 1px rgba(0, 0, 0, 0.08), + inset 0px 1px 2px rgba(0, 0, 0, 0.08); + margin-left: -30px; + } + + &.active { + background: var(--brand6); + color: var(--black0); + z-index: 99; + } + } +} diff --git a/components/Downloads/DownloadToggle/index.stories.tsx b/components/Downloads/DownloadToggle/index.stories.tsx new file mode 100644 index 0000000000000..72ff4a270a27f --- /dev/null +++ b/components/Downloads/DownloadToggle/index.stories.tsx @@ -0,0 +1,37 @@ +import { useState } from 'react'; +import DownloadToggle from './index'; +import type { FC } from 'react'; +import type { DownloadToggleProps } from './index'; +import type { Meta as MetaObj, StoryObj } from '@storybook/react'; + +const Template: FC = ({ selected, showDescription }) => { + const [selectedTypeRelease, setSelectedTypeRelease] = useState(selected); + + const handleTypeReleaseToggle = (value: string) => { + setSelectedTypeRelease(value); + }; + + return ( + + ); +}; + +type Story = StoryObj; +type Meta = MetaObj; + +export const Default: Story = { + args: { selected: 'LTS' }, +}; + +export const Current: Story = { + args: { selected: 'CURRENT' }, +}; + +export default { + component: Template, + args: { showDescription: true }, +} as Meta; diff --git a/components/Downloads/DownloadToggle/index.tsx b/components/Downloads/DownloadToggle/index.tsx new file mode 100644 index 0000000000000..a2a73972efe27 --- /dev/null +++ b/components/Downloads/DownloadToggle/index.tsx @@ -0,0 +1,69 @@ +import { FormattedMessage, useIntl } from 'react-intl'; +import classnames from 'classnames'; +import styles from './index.module.scss'; +import type { FC } from 'react'; + +export type DownloadToggleProps = { + handleClick: (type: string) => void; + selected: string; + showDescription?: boolean; +}; + +const DownloadToggle: FC = ({ + handleClick, + selected, + showDescription = true, +}) => { + const intl = useIntl(); + + const activeClassNames = classnames({ [styles.active]: selected === 'LTS' }); + const currentClassNames = classnames(styles.current, { + [styles.active]: selected === 'CURRENT', + }); + + const handleOnClick = () => + handleClick(selected === 'CURRENT' ? 'LTS' : 'CURRENT'); + + return ( +
    +
    +
    + + +
    +
    + {showDescription && ( +

    + +

    + )} +
    + ); +}; + +export default DownloadToggle; diff --git a/components/Downloads/PrimaryDownloadMatrix.tsx b/components/Downloads/PrimaryDownloadMatrix.tsx new file mode 100644 index 0000000000000..4857b0165635e --- /dev/null +++ b/components/Downloads/PrimaryDownloadMatrix.tsx @@ -0,0 +1,249 @@ +import classNames from 'classnames'; +import semVer from 'semver'; +import LocalizedLink from '../LocalizedLink'; +import { useDetectOS } from '../../hooks/useDetectOS'; +import { useLayoutContext } from '../../hooks/useLayoutContext'; +import { DIST_URL } from '../../next.constants.mjs'; +import type { LegacyDownloadsFrontMatter, NodeRelease } from '../../types'; +import type { FC } from 'react'; + +// @TODO: Instead of using a static list it should be created dynamically. This is done on `nodejs.dev` +// since this is a temporary solution and going to be fixed in the future. +const PrimaryDownloadMatrix: FC = ({ + version, + versionWithPrefix, + isLts, + npm, +}) => { + const { frontMatter } = useLayoutContext(); + + const { bitness } = useDetectOS(); + + const { downloads } = frontMatter as LegacyDownloadsFrontMatter; + const hasWindowsArm64 = semVer.satisfies(version, '>= 19.9.0'); + + const getIsVersionClassName = (isCurrent: boolean) => + classNames({ 'is-version': isCurrent }); + + return ( +
    +

    + {downloads.currentVersion}: {version} ( + {downloads.includes || 'includes'} npm {npm}) +

    +

    {downloads.intro}

    + + + + + + + + + + {hasWindowsArm64 && ( + + )} + + + + + + + {hasWindowsArm64 && ( + + )} + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    {downloads.WindowsInstaller} (.msi) + + 32-bit + + + + 64-bit + + + + ARM64 + +
    {downloads.WindowsBinary} (.zip) + + 32-bit + + + + 64-bit + + + + ARM64 + +
    {downloads.MacOSInstaller} (.pkg) + + 64-bit / ARM64 + +
    {downloads.MacOSBinary} (.tar.gz) + + 64-bit + + + + ARM64 + +
    {downloads.LinuxBinaries} (x64) + + 64-bit + +
    {downloads.LinuxBinaries} (ARM) + + ARMv7 + + + + ARMv8 + +
    {downloads.SourceCode} + + node-{versionWithPrefix}.tar.gz + +
    +
    + ); +}; + +export default PrimaryDownloadMatrix; diff --git a/components/Downloads/SecondaryDownloadMatrix.tsx b/components/Downloads/SecondaryDownloadMatrix.tsx new file mode 100644 index 0000000000000..63b87d1824ba0 --- /dev/null +++ b/components/Downloads/SecondaryDownloadMatrix.tsx @@ -0,0 +1,73 @@ +import DownloadList from './DownloadList'; +import { useLayoutContext } from '../../hooks/useLayoutContext'; +import { WithNodeRelease } from '../../providers/withNodeRelease'; +import { DIST_URL } from '../../next.constants.mjs'; +import type { LegacyDownloadsFrontMatter, NodeRelease } from '../../types'; +import type { FC } from 'react'; + +// @TODO: Instead of using a static list it should be created dynamically. This is done on `nodejs.dev` +// since this is a temporary solution and going to be fixed in the future. +const SecondaryDownloadMatrix: FC = ({ + versionWithPrefix, + status, +}) => { + const { frontMatter } = useLayoutContext(); + + const { additional } = frontMatter as LegacyDownloadsFrontMatter; + + return ( +
    +

    {additional.headline}

    + + + + + + + + + + + + + + + + + + + + + +
    {additional.DockerImage} + + {additional.officialDockerImage} + +
    {additional.LinuxPowerSystems} + + 64-bit + +
    {additional.LinuxSystemZ} + + 64-bit + +
    {additional.AIXPowerSystems} + + 64-bit + +
    + + + {({ release }) => } + +
    + ); +}; + +export default SecondaryDownloadMatrix; diff --git a/components/Footer.tsx b/components/Footer.tsx new file mode 100644 index 0000000000000..eed12f807da8e --- /dev/null +++ b/components/Footer.tsx @@ -0,0 +1,56 @@ +import { FormattedMessage } from 'react-intl'; +import type { FC } from 'react'; + +type FooterProps = { className?: string }; + +// Note.: We don't expect to translate these items as we're going to replace with `nodejs/nodejs.dev` footer +const Footer: FC = ({ className }) => ( + <> + + + ↑ + + + + +); + +export default Footer; diff --git a/components/Header.tsx b/components/Header.tsx new file mode 100644 index 0000000000000..2fc7f2782c20a --- /dev/null +++ b/components/Header.tsx @@ -0,0 +1,113 @@ +import { useIntl } from 'react-intl'; +import Image from 'next/image'; +import classNames from 'classnames'; +import LocalizedLink from './LocalizedLink'; +import { useNavigation } from '../hooks/useNavigation'; +import { useLocale } from '../hooks/useLocale'; +import { useRouter } from '../hooks/useRouter'; + +const Header = () => { + const { availableLocales, isCurrentLocaleRoute } = useLocale(); + const { navigationItems } = useNavigation(); + const { formatMessage } = useIntl(); + const { asPath, basePath } = useRouter(); + + const getLinkClassName = (href: string) => + classNames({ active: isCurrentLocaleRoute(href, href !== '/') }); + + const toggleLanguage = formatMessage({ + id: 'components.header.buttons.toggleLanguage', + }); + + const toggleDarkMode = formatMessage({ + id: 'components.header.buttons.toggleDarkMode', + }); + + const currentRouteLocalized = (locale: string) => + asPath.replace(/^\/[a-zA-Z-]+/, `/${locale}`); + + return ( +
    +
    + + Node.js + + + + +
    + + + +
    + + +
    +
    + ); +}; + +export default Header; diff --git a/components/Home/Banner.tsx b/components/Home/Banner.tsx new file mode 100644 index 0000000000000..5ecfebaa7badd --- /dev/null +++ b/components/Home/Banner.tsx @@ -0,0 +1,37 @@ +import { useSiteConfig } from '../../hooks/useSiteConfig'; +import { dateIsBetween } from '../../util/dateIsBetween'; + +const Banner = () => { + const siteConfig = useSiteConfig(); + + // Note.: This is hardcoded and going to be replaced by the `nodejs/nodejs.dev` codebase + if (siteConfig.websiteBanners && siteConfig.websiteBanners['index']) { + const indexBanner = siteConfig.websiteBanners['index']; + + const showBanner = dateIsBetween( + indexBanner.startDate, + indexBanner.endDate + ); + + if (showBanner && indexBanner.text) { + return ( +

    + {indexBanner.text} +

    + ); + } + + if (showBanner && indexBanner.html) { + return ( + + ); + } + } + + return null; +}; + +export default Banner; diff --git a/components/Home/HomeDownloadButton.tsx b/components/Home/HomeDownloadButton.tsx new file mode 100644 index 0000000000000..c33e40b7c17b4 --- /dev/null +++ b/components/Home/HomeDownloadButton.tsx @@ -0,0 +1,61 @@ +import LocalizedLink from '../LocalizedLink'; +import { useDetectOS } from '../../hooks/useDetectOS'; +import { useLayoutContext } from '../../hooks/useLayoutContext'; +import { downloadUrlByOS } from '../../util/downloadUrlByOS'; +import { getNodejsChangelog } from '../../util/getNodeJsChangelog'; +import { DIST_URL } from '../../next.constants.mjs'; +import type { FC } from 'react'; +import type { NodeRelease } from '../../types'; + +const HomeDownloadButton: FC = ({ + major, + version, + versionWithPrefix, + isLts, +}) => { + const { + frontMatter: { labels }, + } = useLayoutContext(); + + const { os, bitness } = useDetectOS(); + + const nodeDownloadLink = downloadUrlByOS(versionWithPrefix, os, bitness); + const nodeApiLink = `${DIST_URL}latest-v${major}.x/docs/api/`; + const nodeAllDownloadsLink = `/download${isLts ? '/' : '/current'}`; + + const nodeDownloadTitle = `${labels?.download} ${version} ${labels?.[ + isLts ? 'lts' : 'current' + ]}`; + + return ( + + ); +}; + +export default HomeDownloadButton; diff --git a/components/Home/NodeFeatures/NodeFeature/index.module.scss b/components/Home/NodeFeatures/NodeFeature/index.module.scss new file mode 100644 index 0000000000000..278076d33f94f --- /dev/null +++ b/components/Home/NodeFeatures/NodeFeature/index.module.scss @@ -0,0 +1,5 @@ +.container { + margin: 0 2em; + max-width: 248px; + text-align: center; +} diff --git a/components/Home/NodeFeatures/NodeFeature/index.tsx b/components/Home/NodeFeatures/NodeFeature/index.tsx new file mode 100644 index 0000000000000..fb958b5cb88b5 --- /dev/null +++ b/components/Home/NodeFeatures/NodeFeature/index.tsx @@ -0,0 +1,18 @@ +import styles from './index.module.scss'; +import type { ReactElement, FC } from 'react'; + +type NodeFeatureProps = { + icon: ReactElement; + heading: ReactElement; + description: ReactElement; +}; + +const NodeFeature: FC = ({ icon, heading, description }) => ( +
    + {icon} +

    {heading}

    +

    {description}

    +
    +); + +export default NodeFeature; diff --git a/components/Home/NodeFeatures/__snapshots__/index.stories.tsx.snap b/components/Home/NodeFeatures/__snapshots__/index.stories.tsx.snap new file mode 100644 index 0000000000000..33235b15fad35 --- /dev/null +++ b/components/Home/NodeFeatures/__snapshots__/index.stories.tsx.snap @@ -0,0 +1,70 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Home/NodeFeatures Default smoke-test 1`] = ` +
    +
    + + + + + + +

    + JavaScript +

    +

    + Node.js provides support for the JavaScript programming language +

    +
    +
    + + + + +

    + Open Source +

    +

    + Node.js is open source and actively maintained by contributors all over the world +

    +
    +
    + + + + + + +

    + Everywhere +

    +

    + Node.js has been adapted to work in a wide variety of places +

    +
    +
    +`; diff --git a/components/Home/NodeFeatures/index.module.scss b/components/Home/NodeFeatures/index.module.scss new file mode 100644 index 0000000000000..43acfb4083d51 --- /dev/null +++ b/components/Home/NodeFeatures/index.module.scss @@ -0,0 +1,21 @@ +.nodeFeatures { + display: grid; + grid-gap: 1.5rem; + grid-template-columns: repeat(3, 1fr); + justify-content: center; + margin: 0 auto 5em auto; + + @media (max-width: 900px) { + grid-template-columns: 1fr; + } + + .featureIcon { + height: auto; + min-width: 120px; + opacity: 0.75; + + @media (max-width: 900px) { + min-width: 60px; + } + } +} diff --git a/components/Home/NodeFeatures/index.stories.tsx b/components/Home/NodeFeatures/index.stories.tsx new file mode 100644 index 0000000000000..0a36f50493b31 --- /dev/null +++ b/components/Home/NodeFeatures/index.stories.tsx @@ -0,0 +1,9 @@ +import NodeFeatures from '.'; +import type { Meta as MetaObj, StoryObj } from '@storybook/react'; + +type Story = StoryObj; +type Meta = MetaObj; + +export const Default: Story = {}; + +export default { component: NodeFeatures } as Meta; diff --git a/components/Home/NodeFeatures/index.tsx b/components/Home/NodeFeatures/index.tsx new file mode 100644 index 0000000000000..43942adfaf54c --- /dev/null +++ b/components/Home/NodeFeatures/index.tsx @@ -0,0 +1,45 @@ +import { cloneElement } from 'react'; +import { IoLogoNodejs, IoMdGitPullRequest, IoMdRocket } from 'react-icons/io'; +import { FormattedMessage } from 'react-intl'; +import NodeFeature from './NodeFeature'; +import styles from './index.module.scss'; +import type { ReactElement, FC } from 'react'; + +const styled = (icon: ReactElement): ReactElement => + cloneElement(icon, { + alt: 'Node Feature', + className: styles.featureIcon, + }); + +const features = [ + { + icon: styled(), + heading: 'components.home.nodeFeatures.javascript.title', + description: 'components.home.nodeFeatures.javascript.description', + }, + { + icon: styled(), + heading: 'components.home.nodeFeatures.openSource.title', + description: 'components.home.nodeFeatures.openSource.description', + }, + { + icon: styled(), + heading: 'components.home.nodeFeatures.everywhere.title', + description: 'components.home.nodeFeatures.everywhere.description', + }, +]; + +const NodeFeatures: FC = () => ( +
    + {features.map(feature => ( + } + description={} + /> + ))} +
    +); + +export default NodeFeatures; diff --git a/components/HtmlHead.tsx b/components/HtmlHead.tsx new file mode 100644 index 0000000000000..dd5d84a9b878f --- /dev/null +++ b/components/HtmlHead.tsx @@ -0,0 +1,76 @@ +import Head from 'next/head'; +import { useSiteConfig } from '../hooks/useSiteConfig'; +import { useRouter } from '../hooks/useRouter'; +import { BASE_URL, BASE_PATH } from '../next.constants.mjs'; +import type { LegacyFrontMatter } from '../types'; +import type { FC } from 'react'; + +type HeaderProps = { frontMatter: LegacyFrontMatter }; + +const HtmlHead: FC = ({ frontMatter }) => { + const siteConfig = useSiteConfig(); + const { asPath, basePath } = useRouter(); + + const canonicalLink = `${BASE_URL}${BASE_PATH}${asPath}`; + + const pageTitle = frontMatter.title + ? `${frontMatter.title} | ${siteConfig.title}` + : siteConfig.title; + + return ( + + {pageTitle} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {siteConfig.rssFeeds.map(feed => ( + + ))} + + ); +}; + +export default HtmlHead; diff --git a/components/Learn/PreviousNextLink/__snapshots__/index.stories.tsx.snap b/components/Learn/PreviousNextLink/__snapshots__/index.stories.tsx.snap new file mode 100644 index 0000000000000..fcad6477d4047 --- /dev/null +++ b/components/Learn/PreviousNextLink/__snapshots__/index.stories.tsx.snap @@ -0,0 +1,96 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Learn/PreviousNextLink Default smoke-test 1`] = ` + +`; + +exports[`Learn/PreviousNextLink WithoutNext smoke-test 1`] = ` + +`; + +exports[`Learn/PreviousNextLink WithoutPrevious smoke-test 1`] = ` + +`; diff --git a/components/Learn/PreviousNextLink/__tests__/index.test.tsx b/components/Learn/PreviousNextLink/__tests__/index.test.tsx new file mode 100644 index 0000000000000..f2029b4cb879e --- /dev/null +++ b/components/Learn/PreviousNextLink/__tests__/index.test.tsx @@ -0,0 +1,55 @@ +import { render, screen } from '@testing-library/react'; +import { IntlProvider } from 'react-intl'; +import PrevNextLink from '..'; + +jest.mock('next/router', () => ({ + useRouter() { + return { + isReady: true, + asPath: '/link', + }; + }, +})); + +describe('PrevNextLink component', () => { + test('renders nothing if neither previous nor next are provided', () => { + render(); + const component = screen.queryByRole('list'); + expect(component).not.toBeInTheDocument; + }); + + test('renders previous link if previous is provided', () => { + const previous = { slug: '/previous-page' }; + render( + {}}> + + + ); + const link = screen.getByRole('link'); + expect(link).toHaveAttribute('href', `/en${previous.slug}`); + }); + + test('renders next link if next is provided', () => { + const next = { slug: '/next-page' }; + render( + {}}> + + + ); + const link = screen.getByRole('link'); + expect(link).toHaveAttribute('href', `/en${next.slug}`); + }); + + test('renders both previous and next links if both are provided', () => { + const previous = { slug: '/previous-page' }; + const next = { slug: '/next-page' }; + render( + {}}> + + + ); + const links = screen.getAllByRole('link'); + expect(links[0]).toHaveAttribute('href', `/en${previous.slug}`); + expect(links[1]).toHaveAttribute('href', `/en${next.slug}`); + }); +}); diff --git a/components/Learn/PreviousNextLink/index.module.scss b/components/Learn/PreviousNextLink/index.module.scss new file mode 100644 index 0000000000000..98d29f8a1b7e6 --- /dev/null +++ b/components/Learn/PreviousNextLink/index.module.scss @@ -0,0 +1,24 @@ +.prevNextLink { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + list-style: none; + margin: 0; + padding: 5rem 0 0 0; + + a { + align-items: center; + color: var(--black7) !important; + display: flex; + font-family: var(--sans-serif); + font-size: 1.4rem; + font-weight: var(--font-weight-regular); + text-decoration: none !important; + text-transform: uppercase; + vertical-align: middle; + + &:hover { + color: var(--brand-light) !important; + } + } +} diff --git a/components/Learn/PreviousNextLink/index.stories.tsx b/components/Learn/PreviousNextLink/index.stories.tsx new file mode 100644 index 0000000000000..9865c7b2f7f69 --- /dev/null +++ b/components/Learn/PreviousNextLink/index.stories.tsx @@ -0,0 +1,26 @@ +import PrevNextLink from '.'; +import type { Meta as MetaObj, StoryObj } from '@storybook/react'; + +type Story = StoryObj; +type Meta = MetaObj; + +export const Default: Story = { + args: { + previous: { slug: '/previous' }, + next: { slug: '/next' }, + }, +}; + +export const WithoutNext: Story = { + args: { + previous: { slug: '/previous' }, + }, +}; + +export const WithoutPrevious: Story = { + args: { + next: { slug: '/next' }, + }, +}; + +export default { component: PrevNextLink } as Meta; diff --git a/components/Learn/PreviousNextLink/index.tsx b/components/Learn/PreviousNextLink/index.tsx new file mode 100644 index 0000000000000..f6ce34bb2c487 --- /dev/null +++ b/components/Learn/PreviousNextLink/index.tsx @@ -0,0 +1,41 @@ +import { FaAngleDoubleLeft, FaAngleDoubleRight } from 'react-icons/fa'; +import { FormattedMessage } from 'react-intl'; +import LocalizedLink from '../../LocalizedLink'; +import type { LinkInfo } from '../../../types'; +import type { FC } from 'react'; + +import styles from './index.module.scss'; + +type PreviousNextLinkProps = { + previous?: LinkInfo; + next?: LinkInfo; +}; + +const PreviousNextLink: FC = ({ previous, next }) => { + if (!previous && !next) { + return null; + } + + return ( +
      +
    • + {previous && ( + + )} +
    • +
    • + {next && ( + + )} +
    • +
    + ); +}; + +export default PreviousNextLink; diff --git a/components/LocalizedLink.tsx b/components/LocalizedLink.tsx new file mode 100644 index 0000000000000..781e00763f245 --- /dev/null +++ b/components/LocalizedLink.tsx @@ -0,0 +1,45 @@ +import { useMemo } from 'react'; +import Link from 'next/link'; +import { useLocale } from '../hooks/useLocale'; +import { linkWithLocale } from '../util/linkWithLocale'; +import type { FC, ComponentProps } from 'react'; + +// This is a wrapper on HTML's `a` tag +const HtmlLink: FC = ({ children, ...extra }) => ( + {children} +); + +// This is Next.js's Link Component but with pre-fetch disabled +const NextLink: FC> = ({ children, ...extra }) => ( + + {children} + +); + +const LocalizedLink: FC> = ({ + href, + children, + ...extra +}) => { + const { currentLocale } = useLocale(); + + const { Component, finalHref } = useMemo(() => { + if (/^https?:\/\//.test(href.toString())) { + return { Component: HtmlLink, finalHref: href.toString() }; + } + + const addLocaleToHref = linkWithLocale(currentLocale.code); + + return { Component: NextLink, finalHref: addLocaleToHref(href) }; + // We only need to check if the toString() variant of URL has changed + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currentLocale.code, href.toString()]); + + return ( + + {children} + + ); +}; + +export default LocalizedLink; diff --git a/components/Pagination.tsx b/components/Pagination.tsx new file mode 100644 index 0000000000000..989fe40a704e6 --- /dev/null +++ b/components/Pagination.tsx @@ -0,0 +1,23 @@ +import { FormattedMessage } from 'react-intl'; +import LocalizedLink from './LocalizedLink'; +import type { FC } from 'react'; + +type PaginationProps = { prevSlug?: number; nextSlug?: number }; + +const Pagination: FC = ({ nextSlug, prevSlug }) => ( + +); + +export default Pagination; diff --git a/components/Sections/NewFooter/__snapshots__/index.stories.tsx.snap b/components/Sections/NewFooter/__snapshots__/index.stories.tsx.snap new file mode 100644 index 0000000000000..133631c4118ea --- /dev/null +++ b/components/Sections/NewFooter/__snapshots__/index.stories.tsx.snap @@ -0,0 +1,130 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Sections/NewFooter Default smoke-test 1`] = ` + +`; diff --git a/components/Sections/NewFooter/index.module.scss b/components/Sections/NewFooter/index.module.scss new file mode 100644 index 0000000000000..e36924cfeef79 --- /dev/null +++ b/components/Sections/NewFooter/index.module.scss @@ -0,0 +1,102 @@ +.footer { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + margin: 0; + margin-top: auto; + + @media (max-width: 900px) { + flex-direction: column; + } + + .left { + display: flex; + flex-wrap: wrap; + height: 100%; + justify-content: space-between; + list-style: none; + + li a { + color: var(--color-text-primary); + + &:hover { + color: var(--color-brand-primary); + } + } + + > li:first-of-type { + border-right: 1px solid var(--black5); + margin-right: 12px; + padding-right: 12px; + padding-top: 0; + } + + @media (max-width: 900px) { + align-items: center; + flex-direction: column; + height: 10rem; + justify-content: center; + + > li:first-of-type { + border-right: none; + margin-right: 0; + padding: 6px 0; + } + } + } + + .right { + display: flex; + flex-wrap: wrap; + height: 100%; + justify-content: space-between; + list-style: none; + margin-right: 20px; + + > li svg { + height: 1.8rem; + width: 1.8rem; + } + + @media (max-width: 900px) { + margin: 0; + + > li:first-of-type { + margin-left: 0; + margin-right: auto; + } + + > li { + margin-left: 18px; + } + } + } + + .left, + .right { + > li { + font-size: 12px; + height: 24px; + margin-right: 32px; + + @media (max-width: 900px) { + margin-right: 0; + padding: 6px 0; + } + } + + @media (max-width: 900px) { + padding: 6px 18px; + } + } + + .link { + text-decoration: none; + } + + svg { + fill: var(--color-text-secondary); + height: 1.8rem; + width: 1.8rem; + } +} diff --git a/components/Sections/NewFooter/index.stories.tsx b/components/Sections/NewFooter/index.stories.tsx new file mode 100644 index 0000000000000..77f4f96d3109f --- /dev/null +++ b/components/Sections/NewFooter/index.stories.tsx @@ -0,0 +1,9 @@ +import Footer from './index'; +import type { Meta as MetaObj, StoryObj } from '@storybook/react'; + +type Story = StoryObj; +type Meta = MetaObj; + +export const Default: Story = {}; + +export default { component: Footer } as Meta; diff --git a/components/Sections/NewFooter/index.tsx b/components/Sections/NewFooter/index.tsx new file mode 100644 index 0000000000000..34593acf22c2a --- /dev/null +++ b/components/Sections/NewFooter/index.tsx @@ -0,0 +1,103 @@ +import { FormattedMessage } from 'react-intl'; +import { FaGithub, FaSlack, FaTwitter, FaMastodon } from 'react-icons/fa'; +import styles from './index.module.scss'; +import LocalizedLink from '../../LocalizedLink'; + +const Footer = () => ( + +); + +export default Footer; diff --git a/components/Sections/NewHeader/__snapshots__/index.stories.tsx.snap b/components/Sections/NewHeader/__snapshots__/index.stories.tsx.snap new file mode 100644 index 0000000000000..2c1a76617d4ac --- /dev/null +++ b/components/Sections/NewHeader/__snapshots__/index.stories.tsx.snap @@ -0,0 +1,286 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Sections/NewHeader Default smoke-test 1`] = ` + +`; diff --git a/components/Sections/NewHeader/__tests__/index.test.tsx b/components/Sections/NewHeader/__tests__/index.test.tsx new file mode 100644 index 0000000000000..b7ac2336d2753 --- /dev/null +++ b/components/Sections/NewHeader/__tests__/index.test.tsx @@ -0,0 +1,58 @@ +import userEvent from '@testing-library/user-event'; +import { IntlProvider } from 'react-intl'; +import { render, screen } from '@testing-library/react'; +import Header from '..'; + +let mockCurrentTheme = 'light'; + +const mockToggleTheme = jest.fn().mockImplementation(() => { + mockCurrentTheme = mockCurrentTheme === 'dark' ? 'light' : 'dark'; +}); + +// Mock dark mode module for controlling dark mode HOC behaviour +jest.mock('next-themes', () => ({ + useTheme: () => { + return { theme: mockCurrentTheme, setTheme: mockToggleTheme }; + }, +})); + +// mock useRouter +jest.mock('next/router', () => ({ + useRouter() { + return { + locale: 'en', + }; + }, +})); + +jest.mock('../../../../hooks/useLocale', () => ({ + useLocale: () => ({ + currentLocale: { code: 'en', name: 'English', localName: 'English' }, + availableLocales: [ + { code: 'en', name: 'English', localName: 'English' }, + { code: 'es', name: 'Spanish', localName: 'Español' }, + ], + }), +})); + +describe('Tests for Header component', () => { + it('switches logo between light & dark', async () => { + mockCurrentTheme = 'light'; + render( + {}}> +
    + + ); + + const lightLogo = screen.getByAltText('light-logo'); + expect(lightLogo).toBeInTheDocument(); + + const toggle = screen.getByLabelText( + 'components.header.buttons.toggleDarkMode' + ); + await userEvent.click(toggle); + + const darkLogo = screen.getByAltText('dark-logo'); + expect(darkLogo).toBeInTheDocument(); + }); +}); diff --git a/components/Sections/NewHeader/index.module.scss b/components/Sections/NewHeader/index.module.scss new file mode 100644 index 0000000000000..7354954942be3 --- /dev/null +++ b/components/Sections/NewHeader/index.module.scss @@ -0,0 +1,161 @@ +@use 'styles/mixins/screen'; + +%navItemLi { + align-items: center; + cursor: pointer; + display: flex; + margin-left: var(--space-16); + white-space: nowrap; + + @include screen.forTabletPortraitDown { + display: flex; + height: 42px; + margin-left: 0; + + &:not(:last-child) { + margin-right: var(--space-04); + } + } + + > a { + line-height: 0; + } + + &:last-child { + margin-right: var(--space-08); + } +} + +.header { + background: var(--color-fill-app); + border-bottom: var(--space-01) solid var(--black2); + position: sticky; + top: 0; + width: 100%; + + .container { + align-items: center; + display: flex; + flex-direction: row; + justify-content: space-between; + padding: 0 var(--space-08) 0 var(--space-24); + + @include screen.forTabletPortraitDown { + align-items: start; + flex-flow: wrap; + min-height: calc(1 * var(--nav-height)); + padding: 0 var(--space-12); + } + + .startWrapper { + align-items: center; + display: flex; + justify-content: flex-start; + + .logo { + @include screen.forTabletPortraitDown { + align-items: center; + display: flex; + height: var(--nav-height); + } + + svg { + height: var(--space-32); + + @include screen.forTabletPortraitDown { + height: 2.4rem; + } + } + } + } + + .tabs { + align-items: center; + display: flex; + justify-self: flex-start; + list-style: none; + margin: 0 0 0 120px; + margin-right: auto; + + @media (max-width: 1050px) { + margin: 0; + } + + @include screen.forTabletPortraitDown { + background: var(--color-fill-app); + flex-flow: row wrap; + justify-content: center; + order: 3; + padding: 0; + width: 100%; + } + + > li { + @extend %navItemLi; + + > a { + color: var(--color-text-primary); + padding: calc(var(--space-32) + var(--space-04)) var(--space-16); + text-decoration: none; + + @include screen.forTabletPortraitDown { + padding: var(--space-08); + } + + &.active { + border-bottom: var(--space-04) inset var(--color-text-accent); + color: var(--color-text-accent); + font-weight: var(--font-weight-semibold); + margin-bottom: -4px; + } + + &:hover { + border-bottom: 4px solid var(--color-text-primary); + margin-bottom: -4px; + } + } + } + } + + .endWrapper { + align-items: center; + display: flex; + flex-grow: 1; + + > .rightContainer { + align-items: center; + display: flex; + flex: 1 1 auto; + justify-content: flex-end; + list-style: none; + margin: 0; + padding-left: 0; + + @include screen.forTabletPortraitDown { + padding: 0; + width: 100%; + } + + > li { + @extend %navItemLi; + + padding: 10px; + + > a { + color: var(--color-text-accent); + + > svg { + font-size: 2rem; + } + } + } + } + } + } +} + +[data-theme='dark'] { + .header { + border-bottom: 1px solid transparent; + } +} diff --git a/components/Sections/NewHeader/index.stories.tsx b/components/Sections/NewHeader/index.stories.tsx new file mode 100644 index 0000000000000..5372d57f08806 --- /dev/null +++ b/components/Sections/NewHeader/index.stories.tsx @@ -0,0 +1,9 @@ +import Header from './index'; +import type { Meta as MetaObj, StoryObj } from '@storybook/react'; + +type Story = StoryObj; +type Meta = MetaObj; + +export const Default: Story = {}; + +export default { component: Header } as Meta; diff --git a/components/Sections/NewHeader/index.tsx b/components/Sections/NewHeader/index.tsx new file mode 100644 index 0000000000000..93985acc766f3 --- /dev/null +++ b/components/Sections/NewHeader/index.tsx @@ -0,0 +1,92 @@ +import { FormattedMessage } from 'react-intl'; +import Image from 'next/image'; +import { FaGithub } from 'react-icons/fa'; +import LocalizedLink from '../../LocalizedLink'; +import ActiveLocalizedLink from '../../Common/ActiveLocalizedLink'; +import DarkModeToggle from '../../Common/DarkModeToggle'; +import LanguageSelector from '../../Common/LanguageSelector'; +import type { FC } from 'react'; + +import styles from './index.module.scss'; + +const Header: FC = () => ( + +); + +export default Header; diff --git a/components/SideNavigation.tsx b/components/SideNavigation.tsx new file mode 100644 index 0000000000000..dc7647174ea65 --- /dev/null +++ b/components/SideNavigation.tsx @@ -0,0 +1,38 @@ +import classNames from 'classnames'; +import LocalizedLink from './LocalizedLink'; +import { useLocale } from '../hooks/useLocale'; +import { useNavigation } from '../hooks/useNavigation'; +import type { NavigationKeys } from '../types'; +import type { FC } from 'react'; + +type SideNavigationProps = { + navigationKey: NavigationKeys; + context?: Record>; +}; + +const SideNavigation: FC = ({ + navigationKey, + context, +}) => { + const { getSideNavigation } = useNavigation(); + const { isCurrentLocaleRoute } = useLocale(); + + const sideNavigationItems = getSideNavigation(navigationKey, context); + + const getLinkClassName = (href: string) => + classNames({ active: isCurrentLocaleRoute(href) }); + + return ( + + ); +}; + +export default SideNavigation; diff --git a/crowdin.yml b/crowdin.yml index 70e9029bdb92a..60727bc2ceb44 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -1,20 +1,33 @@ +commit_message: 'chore(i18n): added new translations from crowdin' +pull_request_title: 'chore(i18n): crowdin sync' +pull_request_labels: + - i18n + - crowdin files: - - source: /locale/en/**/*.md - translation: /locale/%two_letters_code%/**/%original_file_name% + - source: /pages/en/**/*.md + translation: /pages/%two_letters_code%/**/%original_file_name% content_segmentation: 0 ignore: - - /locale/en/blog/**/*.md - - /locale/en/knowledge/**/*.md + - /pages/en/blog/**/*.md languages_mapping: two_letters_code: pt-BR: pt-br zh-CN: zh-cn zh-TW: zh-tw es-ES: es - - source: /locale/en/**/*.json - translation: /locale/%two_letters_code%/**/%original_file_name% + - source: /pages/en/**/*.mdx + translation: /pages/%two_letters_code%/**/%original_file_name% + content_segmentation: 0 ignore: - - /locale/en/blog/**/*.json + - /pages/en/blog/**/*.mdx + languages_mapping: + two_letters_code: + pt-BR: pt-br + zh-CN: zh-cn + zh-TW: zh-tw + es-ES: es + - source: /i18n/locales/en.json + translation: /i18n/locales/%two_letters_code%.json languages_mapping: two_letters_code: pt-BR: pt-br diff --git a/external/survey-2018/package-lock.json b/external/survey-2018/package-lock.json deleted file mode 100644 index a2d6d0c78d5e5..0000000000000 --- a/external/survey-2018/package-lock.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "survey-report-2018", - "version": "1.0.0", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "commander": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", - "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", - "dev": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "uglify-js": { - "version": "3.3.28", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.3.28.tgz", - "integrity": "sha512-68Rc/aA6cswiaQ5SrE979UJcXX+ADA1z33/ZsPd+fbAiVdjZ16OXdbtGO+rJUUBgK6qdf3SOPhQf3K/ybF5Miw==", - "dev": true, - "requires": { - "commander": "2.15.1", - "source-map": "0.6.1" - } - } - } -} diff --git a/external/survey-2018/package.json b/external/survey-2018/package.json deleted file mode 100644 index f5c798625abc1..0000000000000 --- a/external/survey-2018/package.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "survey-report-2018", - "version": "1.0.0", - "scripts": { - "build": "uglifyjs -c -m -o data.min.js data.js && uglifyjs -c -m -o full.min.js full.js && uglifyjs -c -m -o graph.min.js graph.js" - }, - "devDependencies": { - "uglify-js": "^3.3.28" - } -} diff --git a/global.d.ts b/global.d.ts new file mode 100644 index 0000000000000..597b59a91eb46 --- /dev/null +++ b/global.d.ts @@ -0,0 +1,12 @@ +declare global { + interface Window { + startLegacyApp: Function; + } +} + +declare module '*.json' { + const value: any; + export default value; +} + +export default global; diff --git a/hooks/__tests__/useClickOutside.test.tsx b/hooks/__tests__/useClickOutside.test.tsx new file mode 100644 index 0000000000000..77f10d7b168e0 --- /dev/null +++ b/hooks/__tests__/useClickOutside.test.tsx @@ -0,0 +1,38 @@ +import { render, fireEvent, screen } from '@testing-library/react'; +import { useState } from 'react'; +import { useClickOutside } from '../useClickOutside'; + +describe('useClickOutside', () => { + const Component = () => { + const [state, setState] = useState(false); + const ref = useClickOutside(() => setState(false)); + + return ( +
    +

    Page

    +
    + + {state && ( +
    +

    Modal

    +
    + )} +
    +
    + ); + }; + + it('should call handler when click outside', () => { + render(); + fireEvent.click(screen.getByText('Open')); + fireEvent.click(screen.getByText('Page')); + expect(screen.queryByText('Modal')).not.toBeInTheDocument(); + }); + + it('should not call handler when click inside', () => { + render(); + fireEvent.click(screen.getByText('Open')); + fireEvent.click(screen.getByText('Modal')); + expect(screen.getByText('Modal')).toBeInTheDocument(); + }); +}); diff --git a/hooks/__tests__/useCopyToClipboard.test.tsx b/hooks/__tests__/useCopyToClipboard.test.tsx new file mode 100644 index 0000000000000..04e63466510a4 --- /dev/null +++ b/hooks/__tests__/useCopyToClipboard.test.tsx @@ -0,0 +1,75 @@ +import { render, fireEvent, screen, act } from '@testing-library/react'; +import { FormattedMessage } from 'react-intl'; +import { IntlProvider } from 'react-intl'; +import { useCopyToClipboard } from '../useCopyToClipboard'; + +const mockWriteText = jest.fn(); +const originalNavigator = { ...window.navigator }; + +const testMessages = { + 'components.common.shellBox.copy': + '{copied, select, true {copied}other {copy}}', +}; + +describe('useCopyToClipboard', () => { + beforeEach(() => { + jest.useFakeTimers(); + + Object.defineProperty(window, 'navigator', { + value: { + clipboard: { + writeText: mockWriteText, + }, + }, + }); + }); + + afterEach(() => { + Object.defineProperty(window, 'navigator', { + value: originalNavigator, + }); + }); + + it('should call clipboard API with `test` once', async () => { + const navigatorClipboardWriteTextSpy = jest + .fn() + .mockImplementation(() => Promise.resolve()); + + Object.defineProperty(window.navigator, 'clipboard', { + writable: true, + value: { + writeText: navigatorClipboardWriteTextSpy, + }, + }); + + const TestComponent = ({ textToCopy }: { textToCopy: string }) => { + const [copied, copyText] = useCopyToClipboard(); + + return ( + {}}> + + + ); + }; + + render(); + + const button = screen.getByRole('button'); + + await fireEvent.click(button); + + expect(await screen.findByText(/copied/i)).toBeInTheDocument(); + + act(() => jest.advanceTimersByTime(3000)); + + expect(await screen.findByText(/copy/i)).toBeInTheDocument(); + + expect(navigatorClipboardWriteTextSpy).toHaveBeenCalledTimes(1); + expect(navigatorClipboardWriteTextSpy).toHaveBeenCalledWith('test'); + }); +}); diff --git a/hooks/__tests__/useDetectOS.test.tsx b/hooks/__tests__/useDetectOS.test.tsx new file mode 100644 index 0000000000000..cab6c1c0f4928 --- /dev/null +++ b/hooks/__tests__/useDetectOS.test.tsx @@ -0,0 +1,76 @@ +import { renderHook, waitFor } from '@testing-library/react'; +import { useDetectOS } from '../useDetectOS'; + +const windowsUserAgent = + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36'; + +const macUserAgent = + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36'; + +const originalNavigator = global.navigator; + +describe('useDetectOS', () => { + afterEach(() => { + Object.defineProperty(global, 'navigator', { + value: originalNavigator, + writable: true, + }); + }); + + it('should detect WIN OS and 64 bitness', async () => { + Object.defineProperty(global, 'navigator', { + value: { + userAgent: windowsUserAgent, + userAgentData: { + getHighEntropyValues: jest.fn().mockResolvedValue({ bitness: 64 }), + }, + }, + writable: true, + }); + + const { result } = renderHook(() => useDetectOS()); + + await waitFor(() => { + expect(result.current).toStrictEqual({ + os: 'WIN', + bitness: 64, + }); + }); + }); + + it('should detect WIN OS and 64 bitness from user agent', async () => { + Object.defineProperty(global, 'navigator', { + value: { + userAgent: windowsUserAgent, + }, + writable: true, + }); + + const { result } = renderHook(() => useDetectOS()); + + await waitFor(() => { + expect(result.current).toStrictEqual({ + os: 'WIN', + bitness: 64, + }); + }); + }); + + it('should detect MAC OS and default bitness', async () => { + Object.defineProperty(global, 'navigator', { + value: { + userAgent: macUserAgent, + }, + writable: true, + }); + + const { result } = renderHook(() => useDetectOS()); + + await waitFor(() => { + expect(result.current).toStrictEqual({ + os: 'MAC', + bitness: 86, + }); + }); + }); +}); diff --git a/hooks/__tests__/useMediaQuery.test.tsx b/hooks/__tests__/useMediaQuery.test.tsx new file mode 100644 index 0000000000000..d70c2019cc562 --- /dev/null +++ b/hooks/__tests__/useMediaQuery.test.tsx @@ -0,0 +1,77 @@ +import { renderHook } from '@testing-library/react'; +import { useMediaQuery } from '../useMediaQuery'; + +describe('useMediaQuery', () => { + it('should check for matchMedia support', () => { + const { result } = renderHook(() => useMediaQuery('media-query-mock')); + + expect(result.current).toBe(undefined); + }); + + it('should return true for matched query', () => { + Object.defineProperty(window, 'matchMedia', { + writable: true, + value: () => ({ + matches: true, + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + }), + }); + + const { result } = renderHook(() => useMediaQuery('media-query-mock')); + + expect(result.current).toBe(true); + }); + + it('should return false for not-matched query', () => { + Object.defineProperty(window, 'matchMedia', { + writable: true, + value: () => ({ + matches: false, + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + }), + }); + + const { result } = renderHook(() => useMediaQuery('media-query-mock')); + + expect(result.current).toBe(false); + }); + + it('should subscribe for media changes', () => { + const listenerMock = jest.fn().mockImplementation((_, handler) => { + handler(); + }); + + Object.defineProperty(window, 'matchMedia', { + writable: true, + value: () => ({ + matches: false, + addEventListener: listenerMock, + removeEventListener: jest.fn(), + }), + }); + + renderHook(() => useMediaQuery('media-query-mock')); + + expect(listenerMock).toHaveBeenCalledTimes(1); + }); + + it("should support MediaQueryList's old event listeners", () => { + const listenerMock = jest.fn().mockImplementation(handler => { + handler(); + }); + + Object.defineProperty(window, 'matchMedia', { + writable: true, + value: () => ({ + matches: false, + addListener: listenerMock, + removeListener: jest.fn(), + }), + }); + + renderHook(() => useMediaQuery('media-query-mock')); + expect(listenerMock).toHaveBeenCalledTimes(1); + }); +}); diff --git a/hooks/useBlogData.ts b/hooks/useBlogData.ts new file mode 100644 index 0000000000000..aa3d5f5d48d2d --- /dev/null +++ b/hooks/useBlogData.ts @@ -0,0 +1,54 @@ +import { useCallback, useContext, useMemo } from 'react'; +import { useRouter } from './useRouter'; +import { BlogDataContext } from '../providers/blogDataProvider'; + +export const useBlogData = () => { + const { asPath } = useRouter(); + + const { posts, pagination, categories } = useContext(BlogDataContext); + + const getPostsByCategory = useCallback( + (category: string) => posts.filter(post => post.category === category), + [posts] + ); + + const getPostsByYear = useCallback( + (year: number) => + posts.filter(post => new Date(post.date).getFullYear() === year), + [posts] + ); + + const getPagination = useCallback( + (currentYear: number) => ({ + next: pagination.includes(currentYear + 1) ? currentYear + 1 : undefined, + prev: pagination.includes(currentYear - 1) ? currentYear - 1 : undefined, + }), + [pagination] + ); + + const currentCategory = useMemo(() => { + // We split the pathname to retrieve the blog category from it + // since the URL is usually /{languageCode}/blog/{category} + // the third path piece is usually the category name + const [, , pathname, category] = asPath.split('/'); + + if (pathname === 'blog' && category && category.length) { + return category; + } + + // if either the pathname does not match to a blog page + // which should not happen (as this hook should only be used in blog pages) + // or if there is no category in the URL we return the current year as category name + // which is always the default category (for example, the blog index) + return new Date().getFullYear().toString(); + }, [asPath]); + + return { + posts, + categories, + currentCategory, + getPostsByCategory, + getPostsByYear, + getPagination, + }; +}; diff --git a/hooks/useClickOutside.ts b/hooks/useClickOutside.ts new file mode 100644 index 0000000000000..389fe83a4145c --- /dev/null +++ b/hooks/useClickOutside.ts @@ -0,0 +1,22 @@ +import { useEffect, useRef } from 'react'; + +type UseClickOutsideHandler = (_event: MouseEvent | TouchEvent) => void; + +export const useClickOutside = ( + handler: UseClickOutsideHandler +) => { + const ref = useRef(null); + + useEffect(() => { + const listener = (event: MouseEvent | TouchEvent) => { + if (ref.current && !ref.current.contains(event.target as Node)) { + handler(event); + } + }; + + document.addEventListener('click', listener, true); + + return () => document.removeEventListener('click', listener); + }, [ref, handler]); + return ref; +}; diff --git a/hooks/useCopyToClipboard.ts b/hooks/useCopyToClipboard.ts new file mode 100644 index 0000000000000..bd7b3a4cdeb33 --- /dev/null +++ b/hooks/useCopyToClipboard.ts @@ -0,0 +1,31 @@ +import { useEffect, useState } from 'react'; + +const copyToClipboard = (value: string | undefined) => { + if (!value || typeof navigator === 'undefined') { + return Promise.resolve(false); + } + + return navigator.clipboard + .writeText(value) + .then(() => true) + .catch(() => false); +}; + +export const useCopyToClipboard = () => { + const [copied, setCopied] = useState(false); + + const copyText = (text: string | undefined) => + copyToClipboard(text).then(setCopied); + + useEffect(() => { + if (copied) { + const timerId = setTimeout(() => setCopied(false), 3000); + + return () => clearTimeout(timerId); + } + + return undefined; + }, [copied]); + + return [copied, copyText] as const; +}; diff --git a/hooks/useDetectOS.ts b/hooks/useDetectOS.ts new file mode 100644 index 0000000000000..740a14e603e04 --- /dev/null +++ b/hooks/useDetectOS.ts @@ -0,0 +1,34 @@ +import { useEffect, useState } from 'react'; +import { detectOS } from '../util/detectOS'; +import { getBitness } from '../util/getBitness'; +import type { UserOS } from '../types/userOS'; + +type UserOSState = { + os: UserOS; + bitness: number; +}; + +export const useDetectOS = () => { + const [userOSState, setUserOSState] = useState({ + os: 'OTHER', + bitness: 86, + }); + + useEffect(() => { + getBitness().then(bitness => { + const userAgent = navigator?.userAgent; + + setUserOSState({ + os: detectOS(), + bitness: + bitness === '64' || + userAgent?.includes('WOW64') || + userAgent?.includes('Win64') + ? 64 + : 86, + }); + }); + }, []); + + return userOSState; +}; diff --git a/hooks/useLayoutContext.ts b/hooks/useLayoutContext.ts new file mode 100644 index 0000000000000..a6b3d8c69de6b --- /dev/null +++ b/hooks/useLayoutContext.ts @@ -0,0 +1,4 @@ +import { useContext } from 'react'; +import { LayoutContext } from '../providers/layoutProvider'; + +export const useLayoutContext = () => useContext(LayoutContext); diff --git a/hooks/useLocale.ts b/hooks/useLocale.ts new file mode 100644 index 0000000000000..1a2983b25d1a9 --- /dev/null +++ b/hooks/useLocale.ts @@ -0,0 +1,27 @@ +import { useContext } from 'react'; +import { useRouter } from './useRouter'; +import { LocaleContext } from '../providers/localeProvider'; +import { linkWithLocale } from '../util/linkWithLocale'; + +export const useLocale = () => { + const { currentLocale, availableLocales } = useContext(LocaleContext); + const { asPath } = useRouter(); + + const localizedLink = linkWithLocale(currentLocale.code); + + const localisedPath = (route: string) => + localizedLink(route).replace(/[#|?].*$/, ''); + + return { + availableLocales: availableLocales, + currentLocale: currentLocale, + isCurrentLocaleRoute: (route: string, allowSubPath?: boolean) => { + const localisedRoute = localisedPath(route); + const asPathJustPath = asPath.replace(/[#|?].*$/, ''); + + return allowSubPath + ? asPathJustPath.startsWith(localisedRoute) + : localisedRoute === asPathJustPath; + }, + }; +}; diff --git a/hooks/useMediaQuery.ts b/hooks/useMediaQuery.ts new file mode 100644 index 0000000000000..09206dea3cef3 --- /dev/null +++ b/hooks/useMediaQuery.ts @@ -0,0 +1,40 @@ +import { useState, useEffect } from 'react'; + +const mediaQueryChangeSubscribe = (mq: MediaQueryList, handler: () => void) => { + if (mq.addEventListener) { + mq.addEventListener('change', handler); + } else { + mq.addListener(handler); + } +}; + +const mediaQueryChangeUnsubscribe = ( + mq: MediaQueryList, + handler: () => void +) => { + if (mq.removeEventListener) { + mq.removeEventListener('change', handler); + } else { + mq.removeListener(handler); + } +}; + +export function useMediaQuery(query: string): boolean | undefined { + const [matches, setMatches] = useState(); + + useEffect(() => { + if (typeof window?.matchMedia === 'function') { + const mq = window.matchMedia(query); + setMatches(mq.matches); + + const handler = (): void => setMatches(mq.matches); + mediaQueryChangeSubscribe(mq, handler); + + return (): void => mediaQueryChangeUnsubscribe(mq, handler); + } + + return undefined; + }, [query]); + + return matches; +} diff --git a/hooks/useNavigation.tsx b/hooks/useNavigation.tsx new file mode 100644 index 0000000000000..2a7bea6d1c196 --- /dev/null +++ b/hooks/useNavigation.tsx @@ -0,0 +1,46 @@ +import { FormattedMessage } from 'react-intl'; +import * as nextJson from '../next.json.mjs'; +import type { NavigationEntry, NavigationKeys } from '../types'; + +// Translation Context for FormattedMessage +type Context = Record>; + +// Provides Context replacement for variables within the Link. This is also something that is not going +// to happen in the future with `nodejs/nodejs.dev` codebase +const replaceLinkWithContext = (link: string, context: Record) => + Object.entries(context).reduce( + (finalLink, [find, replace]) => finalLink.replace(`{${find}}`, replace), + link + ); + +export const useNavigation = () => { + const mapNavigationEntries = ( + entries: Record, + context?: Context + ) => { + const getContext = (key: string) => (context && context[key]) || {}; + + const getFormattedMessage = (translationId: string, key: string) => ( + + ); + + return Object.entries(entries).map(([key, item]) => ({ + text: getFormattedMessage(item.translationId, key), + link: replaceLinkWithContext(item.link, getContext(key)), + key: key, + })); + }; + + return { + navigationItems: mapNavigationEntries(nextJson.siteNavigation), + getSideNavigation: (section: NavigationKeys, context?: Context) => + mapNavigationEntries( + // We need the parent and their items when making a side navigation + { + [section]: nextJson.siteNavigation[section], + ...nextJson.siteNavigation[section].items, + }, + context + ), + }; +}; diff --git a/hooks/useNodeReleases.ts b/hooks/useNodeReleases.ts new file mode 100644 index 0000000000000..24e13138da131 --- /dev/null +++ b/hooks/useNodeReleases.ts @@ -0,0 +1,15 @@ +import { useCallback, useContext } from 'react'; +import { NodeReleasesContext } from '../providers/nodeReleasesProvider'; +import type { NodeReleaseStatus } from '../types'; + +export const useNodeReleases = () => { + const releases = useContext(NodeReleasesContext); + + const getReleaseByStatus = useCallback( + (status: NodeReleaseStatus) => + releases.find(release => release.status === status), + [releases] + ); + + return { releases, getReleaseByStatus }; +}; diff --git a/hooks/useRouter.ts b/hooks/useRouter.ts new file mode 100644 index 0000000000000..37b8179222976 --- /dev/null +++ b/hooks/useRouter.ts @@ -0,0 +1,25 @@ +import { useMemo } from 'react'; +import { useRouter as useNextRouter } from 'next/router'; +import * as nextLocales from '../next.locales.mjs'; +import type { NextRouter } from 'next/router'; + +// Maps all available locales by only their Language Code +const mappedLocalesByCode = nextLocales.availableLocales.map(l => l.code); + +export const useRouter = (): NextRouter => { + const router = useNextRouter(); + + return useMemo(() => { + const currentLocale = nextLocales.getCurrentLocale( + router.asPath, + router.query + ); + + return { + ...router, + locale: currentLocale.code, + locales: mappedLocalesByCode, + defaultLocale: nextLocales.defaultLocale.code, + }; + }, [router]); +}; diff --git a/hooks/useSiteConfig.ts b/hooks/useSiteConfig.ts new file mode 100644 index 0000000000000..8f18795dcca6f --- /dev/null +++ b/hooks/useSiteConfig.ts @@ -0,0 +1,8 @@ +import { useContext } from 'react'; +import { SiteContext } from '../providers/siteProvider'; + +export const useSiteConfig = () => { + const siteConfigContext = useContext(SiteContext); + + return siteConfigContext; +}; diff --git a/i18n/config.json b/i18n/config.json new file mode 100644 index 0000000000000..1b682d7654944 --- /dev/null +++ b/i18n/config.json @@ -0,0 +1,174 @@ +[ + { + "code": "ar", + "localName": "العربية", + "name": "Arabic", + "langDir": "rtl", + "dateFormat": "YYYY.MM.DD", + "hrefLang": "ar", + "enabled": true + }, + { + "code": "ca", + "localName": "Catalan", + "name": "Catalan", + "langDir": "ltr", + "dateFormat": "DD.MM.YYYY", + "hrefLang": "ca", + "enabled": true + }, + { + "code": "de", + "localName": "Deutsch", + "name": "German", + "langDir": "ltr", + "dateFormat": "DD.MM.YYYY", + "hrefLang": "de", + "enabled": true + }, + { + "code": "en", + "localName": "English", + "name": "English", + "langDir": "ltr", + "dateFormat": "MM.DD.YYYY", + "hrefLang": "en-GB", + "enabled": true, + "default": true + }, + { + "code": "es", + "localName": "Español", + "name": "Spanish", + "langDir": "ltr", + "dateFormat": "DD.MM.YYYY", + "hrefLang": "es-ES", + "enabled": true + }, + { + "code": "fa", + "localName": "زبان فارسی", + "name": "Persian", + "langDir": "rtl", + "dateFormat": "YYYY.MM.DD", + "hrefLang": "fa", + "enabled": true + }, + { + "code": "fr", + "localName": "Français", + "name": "French", + "langDir": "ltr", + "dateFormat": "DD.MM.YYYY", + "hrefLang": "fr", + "enabled": true + }, + { + "code": "id", + "localName": "Bahasa Indonesia", + "name": "Indonesian", + "langDir": "ltr", + "dateFormat": "DD.MM.YYYY", + "hrefLang": "id", + "enabled": true + }, + { + "code": "it", + "localName": "Italiano", + "name": "Italian", + "langDir": "ltr", + "dateFormat": "DD.MM.YYYY", + "hrefLang": "it", + "enabled": true + }, + { + "code": "ja", + "localName": "日本語", + "name": "Japanese", + "langDir": "ltr", + "dateFormat": "YYYY.MM.DD", + "hrefLang": "ja", + "enabled": true + }, + { + "code": "ka", + "localName": "ქართული", + "name": "Georgian", + "langDir": "ltr", + "dateFormat": "DD.MM.YYYY", + "hrefLang": "ka", + "enabled": true + }, + { + "code": "ko", + "localName": "한국어", + "name": "Korean", + "langDir": "ltr", + "dateFormat": "YYYY.MM.DD", + "hrefLang": "ko", + "enabled": true + }, + { + "code": "pt-br", + "localName": "Português do Brasil", + "name": "Brazilian Portuguese", + "langDir": "ltr", + "dateFormat": "DD.MM.YYYY", + "hrefLang": "", + "enabled": true + }, + { + "code": "ro", + "localName": "limba română", + "name": "Romanian", + "langDir": "ltr", + "dateFormat": "DD.MM.YYYY", + "hrefLang": "ro", + "enabled": true + }, + { + "code": "ru", + "localName": "Русский", + "name": "Russian", + "langDir": "ltr", + "dateFormat": "DD.MM.YYYY", + "hrefLang": "ru", + "enabled": true + }, + { + "code": "tr", + "localName": "Türkçe", + "name": "Turkish", + "langDir": "ltr", + "dateFormat": "DD.MM.YYYY", + "hrefLang": "tr", + "enabled": true + }, + { + "code": "uk", + "localName": "Українська", + "name": "Ukrainian", + "langDir": "ltr", + "dateFormat": "DD.MM.YYYY", + "hrefLang": "uk", + "enabled": true + }, + { + "code": "zh-cn", + "localName": "简体中文", + "name": "Simplified Chinese", + "langDir": "ltr", + "dateFormat": "YYYY/MM/DD", + "hrefLang": "zh-Hans", + "enabled": true + }, + { + "code": "zh-tw", + "localName": "繁體中文", + "name": "Traditional Chinese", + "langDir": "ltr", + "dateFormat": "YYYY/MM/DD", + "hrefLang": "zh-Hant", + "enabled": true + } +] diff --git a/i18n/locales/ar.json b/i18n/locales/ar.json new file mode 100644 index 0000000000000..6c019f198cab0 --- /dev/null +++ b/i18n/locales/ar.json @@ -0,0 +1,39 @@ +{ + "components.footer.scrollToTop.button": "إنتقل إلى الأعلى", + "components.header.links.home": "الرئيسية", + "components.header.links.about": "عن الموقع", + "components.header.links.download": "تنزيلات", + "components.header.links.docs": "التوثيق", + "components.header.links.getInvolved": "إنضم إلينا", + "components.header.links.security": "أمن", + "components.header.links.certification": "شهادة", + "components.header.links.blog": "المدونة", + "components.navigation.about.links.governance": "الحوكمة", + "components.navigation.docs.links.es6": "ES6 وما بعدها", + "components.navigation.docs.links.apiLts": "{fullLtsNodeVersion} API {spanLts}", + "components.navigation.docs.links.apiCurrent": "{fullCurrentNodeVersion} API", + "components.navigation.docs.links.guides": "إرشادات", + "components.navigation.docs.links.dependencies": "تبعيات", + "components.navigation.getInvolved.links.collabSummit": "القمة التعاونية", + "components.navigation.getInvolved.links.contribute": "مساهمة", + "components.navigation.getInvolved.links.codeOfConduct": "القواعد السلوكية", + "components.downloadList.links.previousReleases": "الاصدارات السابقة", + "components.downloadList.links.packageManager": "تحميل Node.js عن طريق نظام إدارة الحزم الافتراضي", + "components.downloadList.links.shaSums": "وثائق الاصدارات مختومة SHASUMS", + "components.downloadList.links.shaSums.howToVerify": " (كيفية التحقق)", + "components.downloadList.links.allDownloads": "تحميل جميع النسخ", + "components.downloadList.links.nightlyReleases": "إصدارات ليلية", + "components.downloadList.links.unofficialBuilds": "بُنْيَات غير رسمية", + "components.downloadList.links.buildingFromSource": "Building Node.js from source on supported platforms", + "components.downloadList.links.installingOnLinux": "تثبيت Node.js باستعمال أرشيف ثنائي على لينكس", + "components.downloadList.links.installingOnWsl": "التثبيت على ويندوز في النظام الفرعي لينكس (WSL)", + "components.downloadReleasesTable.changelog": "سجل التغييرات", + "components.downloadReleasesTable.releases": "Releases", + "components.downloadReleasesTable.docs": "التوثيق", + "components.header.buttons.toggleLanguage": "Toggle Language", + "components.header.buttons.toggleDarkMode": "Toggle dark/light mode", + "components.pagination.next": "التالي | ", + "components.pagination.previous": "السابق", + "layouts.blogPost.author.byLine": "{author, select, null {} other {By {author}, }}", + "layouts.blogIndex.currentYear": "News from {year}" +} diff --git a/i18n/locales/ca.json b/i18n/locales/ca.json new file mode 100644 index 0000000000000..c1c574cedb3c3 --- /dev/null +++ b/i18n/locales/ca.json @@ -0,0 +1,39 @@ +{ + "components.footer.scrollToTop.button": "Torna al començament", + "components.header.links.home": "Inici", + "components.header.links.about": "Quant a", + "components.header.links.download": "Descàrregues", + "components.header.links.docs": "Documentació", + "components.header.links.getInvolved": "Participa", + "components.header.links.security": "Seguretat", + "components.header.links.certification": "Certification", + "components.header.links.blog": "Notícies", + "components.navigation.about.links.governance": "Govern", + "components.navigation.docs.links.es6": "ES6 i més enllà", + "components.navigation.docs.links.apiLts": "{fullLtsNodeVersion} API {spanLts}", + "components.navigation.docs.links.apiCurrent": "{fullCurrentNodeVersion} API", + "components.navigation.docs.links.guides": "Guies", + "components.navigation.docs.links.dependencies": "Dependències", + "components.navigation.getInvolved.links.collabSummit": "Col·labora en el Summit", + "components.navigation.getInvolved.links.contribute": "Contribueix", + "components.navigation.getInvolved.links.codeOfConduct": "Codi de Conducta", + "components.downloadList.links.previousReleases": "Descarrègues", + "components.downloadList.links.packageManager": "Instal·lar Node.js mitjançant un gestor de paquets", + "components.downloadList.links.shaSums": "Signatures SHASUMS per arxius de versions", + "components.downloadList.links.shaSums.howToVerify": " (Com verificar-ho)", + "components.downloadList.links.allDownloads": "Totes les opcions de descàrrega", + "components.downloadList.links.nightlyReleases": "Versions Nightly", + "components.downloadList.links.unofficialBuilds": "Unofficial builds", + "components.downloadList.links.buildingFromSource": "Building Node.js from source on supported platforms", + "components.downloadList.links.installingOnLinux": "Installing Node.js via binary archive", + "components.downloadList.links.installingOnWsl": "Install on Windows Subsystem for Linux (WSL)", + "components.downloadReleasesTable.changelog": "Registre de Canvis", + "components.downloadReleasesTable.releases": "Releases", + "components.downloadReleasesTable.docs": "Documentació", + "components.header.buttons.toggleLanguage": "Toggle Language", + "components.header.buttons.toggleDarkMode": "Toggle dark/light mode", + "components.pagination.next": "Següent | ", + "components.pagination.previous": "Anterior", + "layouts.blogPost.author.byLine": "{author, select, null {} other {By {author}, }}", + "layouts.blogIndex.currentYear": "News from {year}" +} diff --git a/i18n/locales/de.json b/i18n/locales/de.json new file mode 100644 index 0000000000000..8694496e07c5f --- /dev/null +++ b/i18n/locales/de.json @@ -0,0 +1,39 @@ +{ + "components.footer.scrollToTop.button": "Zum Seitenanfang", + "components.header.links.home": "Startseite", + "components.header.links.about": "Über Node.js", + "components.header.links.download": "Herunterladen", + "components.header.links.docs": "Dokumentation", + "components.header.links.getInvolved": "Mitmachen", + "components.header.links.security": "Sicherheit", + "components.header.links.certification": "Zertifizierung", + "components.header.links.blog": "Neuigkeiten", + "components.navigation.about.links.governance": "Governance", + "components.navigation.docs.links.es6": "ES6 und darüber hinaus", + "components.navigation.docs.links.apiLts": "{fullLtsNodeVersion} API {spanLts}", + "components.navigation.docs.links.apiCurrent": "{fullCurrentNodeVersion} API", + "components.navigation.docs.links.guides": "Anleitungen", + "components.navigation.docs.links.dependencies": "Abhängigkeiten", + "components.navigation.getInvolved.links.collabSummit": "Collab Summit", + "components.navigation.getInvolved.links.contribute": "Mitwirken", + "components.navigation.getInvolved.links.codeOfConduct": "Verhaltenskodex", + "components.downloadList.links.previousReleases": "Alle Versionen", + "components.downloadList.links.packageManager": "Node.js mit Paketmanagern installieren", + "components.downloadList.links.shaSums": "Signierte SHASUMS für die Versionsdateien", + "components.downloadList.links.shaSums.howToVerify": " (wie Überprüfen?)", + "components.downloadList.links.allDownloads": "Alle Download-Optionen", + "components.downloadList.links.nightlyReleases": "Nightly Builds", + "components.downloadList.links.unofficialBuilds": "Inoffizielle Builds", + "components.downloadList.links.buildingFromSource": "Erstellen von Node.js aus dem Quellcode auf unterstützten Plattformen", + "components.downloadList.links.installingOnLinux": "Installieren von Node.js über ein Binärarchiv", + "components.downloadList.links.installingOnWsl": "Auf Windows-Subsystem für Linux (WSL) installieren", + "components.downloadReleasesTable.changelog": "Änderungsprotokoll", + "components.downloadReleasesTable.releases": "Veröffentlichungen", + "components.downloadReleasesTable.docs": "Dokumentation", + "components.header.buttons.toggleLanguage": "Sprache umschalten", + "components.header.buttons.toggleDarkMode": "Dunkel-/Hell Modus umschalten", + "components.pagination.next": "Neuere | ", + "components.pagination.previous": "Ältere", + "layouts.blogPost.author.byLine": "{author, select, null {} other {Von {author}, }}", + "layouts.blogIndex.currentYear": "Neuigkeiten von {year}" +} diff --git a/i18n/locales/en.json b/i18n/locales/en.json new file mode 100644 index 0000000000000..7f42d271d3f8a --- /dev/null +++ b/i18n/locales/en.json @@ -0,0 +1,82 @@ +{ + "components.footer.scrollToTop.button": "Scroll to top", + "components.header.links.home": "Home", + "components.header.links.about": "About", + "components.header.links.download": "Downloads", + "components.header.links.docs": "Docs", + "components.header.links.getInvolved": "Get Involved", + "components.header.links.security": "Security", + "components.header.links.certification": "Certification", + "components.header.links.blog": "News", + "components.navigation.about.links.governance": "Governance", + "components.navigation.docs.links.es6": "ES6 and beyond", + "components.navigation.docs.links.apiLts": "{fullLtsNodeVersion} API {spanLts}", + "components.navigation.docs.links.apiCurrent": "{fullCurrentNodeVersion} API", + "components.navigation.docs.links.guides": "Guides", + "components.navigation.docs.links.dependencies": "Dependencies", + "components.navigation.getInvolved.links.collabSummit": "Collab Summit", + "components.navigation.getInvolved.links.contribute": "Contribute", + "components.navigation.getInvolved.links.codeOfConduct": "Code of Conduct", + "components.downloadList.links.previousReleases": "Previous Releases", + "components.downloadList.links.packageManager": "Installing Node.js via package manager", + "components.downloadList.links.shaSums": "Signed SHASUMS for release files", + "components.downloadList.links.shaSums.howToVerify": " (How to verify)", + "components.downloadList.links.allDownloads": "All download options", + "components.downloadList.links.nightlyReleases": "Nightly builds", + "components.downloadList.links.unofficialBuilds": "Unofficial builds", + "components.downloadList.links.buildingFromSource": "Building Node.js from source on supported platforms", + "components.downloadList.links.installingOnLinux": "Installing Node.js via binary archive", + "components.downloadList.links.installingOnWsl": "Install on Windows Subsystem for Linux (WSL)", + "components.downloadReleasesTable.changelog": "Changelog", + "components.downloadReleasesTable.releases": "Releases", + "components.downloadReleasesTable.docs": "Docs", + "components.header.buttons.toggleLanguage": "Toggle Language", + "components.header.buttons.toggleDarkMode": "Toggle dark/light mode", + "components.pagination.next": "Newer | ", + "components.pagination.previous": "Older", + "layouts.blogPost.author.byLine": "{author, select, null {} other {By {author}, }}", + "layouts.blogIndex.currentYear": "News from {year}", + "components.common.banner.button.text": "Read More", + "components.article.author.githubLinkLabel": "{username} Github - opens in new tab", + "components.article.authorList.title": "Article Authors", + "components.article.editLink.title.edit": "Edit this page on GitHub", + "components.article.editLink.title.translate": "Interested to help with translations?", + "components.common.languageSelector.button.title": "Switch Language", + "components.blog.blogCard.author.by": "by", + "components.sections.newFooter.trademark": "Trademark Policy", + "components.sections.newFooter.privacy": "Privacy Policy", + "components.sections.newFooter.codeOfConduct": "Code of Conduct", + "components.sections.newFooter.security": "Security", + "components.sections.newFooter.about": "About", + "components.sections.newFooter.blog": "Blog", + "components.sections.newFooter.github": "GitHub", + "components.learn.previousNextLink.next": "NEXT", + "components.learn.previousNextLink.previous": "PREV", + "components.home.nodeFeatures.javascript.title": "JavaScript", + "components.home.nodeFeatures.javascript.description": "Node.js provides support for the JavaScript programming language", + "components.home.nodeFeatures.openSource.title": "Open Source", + "components.home.nodeFeatures.openSource.description": "Node.js is open source and actively maintained by contributors all over the world", + "components.home.nodeFeatures.everywhere.title": "Everywhere", + "components.home.nodeFeatures.everywhere.description": "Node.js has been adapted to work in a wide variety of places", + "components.common.shellBox.copy": "{copied, select, true {copied}other {copy}}", + "components.codeBox.copy": "{copied, select, true {copied}other {copy}}", + "components.api.stability": "Stability: {level} - ", + "components.api.jsonLink.title": "View as JSON", + "components.api.sourceLink": "Source Code:", + "components.sections.newHeader.links.learn": "Learn", + "components.sections.newHeader.links.apiDocs": "API Docs", + "components.sections.newHeader.links.about": "About", + "components.sections.newHeader.links.download": "Downloads", + "components.sections.newHeader.links.certification": "Certification", + "components.downloads.downloadToggle.current": "Current", + "components.downloads.downloadToggle.lts": "LTS", + "components.downloads.downloadToggle.recommendation": "{selected, select, LTS {Recommended for most users}other {With the latest features}}", + "components.downloads.downloadToggle.ltsVersions": "LTS Versions", + "components.downloads.downloadToggle.currentVersions": "Current Versions", + "pages.404.title": "404: Page could not be found", + "pages.404.description": "ENOENT: no such file or directory", + "components.api.apiChanges.addedIn": "Added in: {version}", + "components.api.apiChanges.history": "History", + "components.api.apiChanges.history.version": "Version", + "components.api.apiChanges.history.changes": "Changes" +} diff --git a/i18n/locales/es.json b/i18n/locales/es.json new file mode 100644 index 0000000000000..261a095549676 --- /dev/null +++ b/i18n/locales/es.json @@ -0,0 +1,39 @@ +{ + "components.footer.scrollToTop.button": "Vuelve arriba", + "components.header.links.home": "Inicio", + "components.header.links.about": "Acerca", + "components.header.links.download": "Descargas", + "components.header.links.docs": "Documentación", + "components.header.links.getInvolved": "Participe", + "components.header.links.security": "Seguridad", + "components.header.links.certification": "Certification", + "components.header.links.blog": "Noticias", + "components.navigation.about.links.governance": "Dirección", + "components.navigation.docs.links.es6": "ES6 y más allá", + "components.navigation.docs.links.apiLts": "{fullLtsNodeVersion} API {spanLts}", + "components.navigation.docs.links.apiCurrent": "{fullCurrentNodeVersion} API", + "components.navigation.docs.links.guides": "Guías", + "components.navigation.docs.links.dependencies": "Dependencias", + "components.navigation.getInvolved.links.collabSummit": "Colabore en el Summit", + "components.navigation.getInvolved.links.contribute": "Contribuya", + "components.navigation.getInvolved.links.codeOfConduct": "Código de Conducta", + "components.downloadList.links.previousReleases": "Versiones anteriores", + "components.downloadList.links.packageManager": "Instale Node.js mediante un gestor de paquetes", + "components.downloadList.links.shaSums": "Firmas SHASUMS de los archivos de versiones", + "components.downloadList.links.shaSums.howToVerify": " (Cómo verificarlo)", + "components.downloadList.links.allDownloads": "Todas las opciones de descarga", + "components.downloadList.links.nightlyReleases": "Versiones Nightly", + "components.downloadList.links.unofficialBuilds": "Construcciones no oficiales", + "components.downloadList.links.buildingFromSource": "Building Node.js from source on supported platforms", + "components.downloadList.links.installingOnLinux": "Instalación de Node.js a través del archivo binario", + "components.downloadList.links.installingOnWsl": "Install on Windows Subsystem for Linux (WSL)", + "components.downloadReleasesTable.changelog": "Registro de Cambios", + "components.downloadReleasesTable.releases": "Releases", + "components.downloadReleasesTable.docs": "Documentación", + "components.header.buttons.toggleLanguage": "Toggle Language", + "components.header.buttons.toggleDarkMode": "Toggle dark/light mode", + "components.pagination.next": "Siguiente | ", + "components.pagination.previous": "Anterior", + "layouts.blogPost.author.byLine": "{author, select, null {} other {By {author}, }}", + "layouts.blogIndex.currentYear": "News from {year}" +} diff --git a/i18n/locales/fa.json b/i18n/locales/fa.json new file mode 100644 index 0000000000000..c4ea7b173e3ed --- /dev/null +++ b/i18n/locales/fa.json @@ -0,0 +1,39 @@ +{ + "components.footer.scrollToTop.button": "رفتن به بالا", + "components.header.links.home": "خانه", + "components.header.links.about": "درباره", + "components.header.links.download": "دانلود‌ها", + "components.header.links.docs": "اسناد", + "components.header.links.getInvolved": "مشارکت جستن", + "components.header.links.security": "امنیت", + "components.header.links.certification": "Certification", + "components.header.links.blog": "بلاگ", + "components.navigation.about.links.governance": "مدیریت", + "components.navigation.docs.links.es6": "ES6 و فراتر", + "components.navigation.docs.links.apiLts": "{fullLtsNodeVersion} API {spanLts}", + "components.navigation.docs.links.apiCurrent": "{fullCurrentNodeVersion} API", + "components.navigation.docs.links.guides": "راهنمایی‌ها", + "components.navigation.docs.links.dependencies": "وابستگی‌ها", + "components.navigation.getInvolved.links.collabSummit": "نشت مشارکت کنندگان", + "components.navigation.getInvolved.links.contribute": "مشارکت", + "components.navigation.getInvolved.links.codeOfConduct": "شیوه تعامل برای مشارکت", + "components.downloadList.links.previousReleases": "انتشارهای پیشین", + "components.downloadList.links.packageManager": "نصب نودجی‌اس با package manager", + "components.downloadList.links.shaSums": "Signed SHASUMS for release files", + "components.downloadList.links.shaSums.howToVerify": " (چگونه راستی‌آزمایی کنیم؟)", + "components.downloadList.links.allDownloads": "تمام گزینه‌ها برای دانلود", + "components.downloadList.links.nightlyReleases": "ساخت‌های شبانه", + "components.downloadList.links.unofficialBuilds": "Unofficial builds", + "components.downloadList.links.buildingFromSource": "Building Node.js from source on supported platforms", + "components.downloadList.links.installingOnLinux": "Installing Node.js via binary archive", + "components.downloadList.links.installingOnWsl": "Install on Windows Subsystem for Linux (WSL)", + "components.downloadReleasesTable.changelog": "تغییرات", + "components.downloadReleasesTable.releases": "Releases", + "components.downloadReleasesTable.docs": "اسناد", + "components.header.buttons.toggleLanguage": "Toggle Language", + "components.header.buttons.toggleDarkMode": "Toggle dark/light mode", + "components.pagination.next": "بعدی | ", + "components.pagination.previous": "قبلی", + "layouts.blogPost.author.byLine": "{author, select, null {} other {By {author}, }}", + "layouts.blogIndex.currentYear": "News from {year}" +} diff --git a/i18n/locales/fr.json b/i18n/locales/fr.json new file mode 100644 index 0000000000000..4dd29b8c21700 --- /dev/null +++ b/i18n/locales/fr.json @@ -0,0 +1,39 @@ +{ + "components.footer.scrollToTop.button": "Faire défiler en haut", + "components.header.links.home": "Accueil", + "components.header.links.about": "À propos", + "components.header.links.download": "Téléchargements", + "components.header.links.docs": "Docs", + "components.header.links.getInvolved": "S’impliquer", + "components.header.links.security": "Securité", + "components.header.links.certification": "Certification", + "components.header.links.blog": "Actualités", + "components.navigation.about.links.governance": "Gouvernance", + "components.navigation.docs.links.es6": "ES6 et au-delà", + "components.navigation.docs.links.apiLts": "{fullLtsNodeVersion} API {spanLts}", + "components.navigation.docs.links.apiCurrent": "{fullCurrentNodeVersion} API", + "components.navigation.docs.links.guides": "Guides", + "components.navigation.docs.links.dependencies": "Dépendances", + "components.navigation.getInvolved.links.collabSummit": "Sommet de collaboration", + "components.navigation.getInvolved.links.contribute": "Contribuer", + "components.navigation.getInvolved.links.codeOfConduct": "Code de conduite", + "components.downloadList.links.previousReleases": "Versions précédentes", + "components.downloadList.links.packageManager": "Installer Node.js via le gestionnaire de paquets", + "components.downloadList.links.shaSums": "SHASUMS signés pour les fichiers des versions", + "components.downloadList.links.shaSums.howToVerify": " (Comment vérifier)", + "components.downloadList.links.allDownloads": "Toutes les options de téléchargement", + "components.downloadList.links.nightlyReleases": "Versions quotidiennes", + "components.downloadList.links.unofficialBuilds": "Constructions non officielles", + "components.downloadList.links.buildingFromSource": "Compiler Node.js à partir du code source sur les systèmes d'exploitation maintenus", + "components.downloadList.links.installingOnLinux": "Installation de Node.js via une archive binaire", + "components.downloadList.links.installingOnWsl": "Installation sur le sous-système Windows pour Linux (WSL)", + "components.downloadReleasesTable.changelog": "Journal des modifications", + "components.downloadReleasesTable.releases": "Distributions", + "components.downloadReleasesTable.docs": "Docs", + "components.header.buttons.toggleLanguage": "Changer la langue", + "components.header.buttons.toggleDarkMode": "Basculer le mode sombre/clair", + "components.pagination.next": "Plus récent ", + "components.pagination.previous": "Plus anciens", + "layouts.blogPost.author.byLine": "{author, select, null {} other {Par {author}, }}", + "layouts.blogIndex.currentYear": "Actualités de {year}" +} diff --git a/i18n/locales/gl.json b/i18n/locales/gl.json new file mode 100644 index 0000000000000..90589c897c616 --- /dev/null +++ b/i18n/locales/gl.json @@ -0,0 +1,39 @@ +{ + "components.footer.scrollToTop.button": "Volta ao principio", + "components.header.links.home": "Inicio", + "components.header.links.about": "Acerca de", + "components.header.links.download": "Descargas", + "components.header.links.docs": "Documentación", + "components.header.links.getInvolved": "Participa", + "components.header.links.security": "Seguridade", + "components.header.links.certification": "Certification", + "components.header.links.blog": "Noticias", + "components.navigation.about.links.governance": "Goberno", + "components.navigation.docs.links.es6": "ES6 e máis aló", + "components.navigation.docs.links.apiLts": "{fullLtsNodeVersion} API {spanLts}", + "components.navigation.docs.links.apiCurrent": "{fullCurrentNodeVersion} API", + "components.navigation.docs.links.guides": "Guías", + "components.navigation.docs.links.dependencies": "Dependencies", + "components.navigation.getInvolved.links.collabSummit": "Collab Summit", + "components.navigation.getInvolved.links.contribute": "Contribúe", + "components.navigation.getInvolved.links.codeOfConduct": "Comportamento", + "components.downloadList.links.previousReleases": "Versións anteriores", + "components.downloadList.links.packageManager": "Instalar Node.js usando un xestor de paquetes", + "components.downloadList.links.shaSums": "Firmas SHASUMS para os arquivos de versións", + "components.downloadList.links.shaSums.howToVerify": " (How to verify)", + "components.downloadList.links.allDownloads": "Todas as opcións de descarga", + "components.downloadList.links.nightlyReleases": "Versións Nightly", + "components.downloadList.links.unofficialBuilds": "Unofficial builds", + "components.downloadList.links.buildingFromSource": "Building Node.js from source on supported platforms", + "components.downloadList.links.installingOnLinux": "Installing Node.js via binary archive", + "components.downloadList.links.installingOnWsl": "Install on Windows Subsystem for Linux (WSL)", + "components.downloadReleasesTable.changelog": "Cambios", + "components.downloadReleasesTable.releases": "Releases", + "components.downloadReleasesTable.docs": "Documentación", + "components.header.buttons.toggleLanguage": "Toggle Language", + "components.header.buttons.toggleDarkMode": "Toggle dark/light mode", + "components.pagination.next": "Seguinte | ", + "components.pagination.previous": "Anterior", + "layouts.blogPost.author.byLine": "{author, select, null {} other {By {author}, }}", + "layouts.blogIndex.currentYear": "News from {year}" +} diff --git a/i18n/locales/id.json b/i18n/locales/id.json new file mode 100644 index 0000000000000..57b1edde547ad --- /dev/null +++ b/i18n/locales/id.json @@ -0,0 +1,82 @@ +{ + "components.footer.scrollToTop.button": "Gulir ke atas", + "components.header.links.home": "Beranda", + "components.header.links.about": "Tentang", + "components.header.links.download": "Unduh", + "components.header.links.docs": "Dokumen", + "components.header.links.getInvolved": "Berkontribusi", + "components.header.links.security": "Keamanan", + "components.header.links.certification": "Sertifikasi", + "components.header.links.blog": "Berita", + "components.navigation.about.links.governance": "Tata Kerja", + "components.navigation.docs.links.es6": "ES6 dan seterusnya", + "components.navigation.docs.links.apiLts": "{fullLtsNodeVersion} API {spanLts}", + "components.navigation.docs.links.apiCurrent": "{fullCurrentNodeVersion} API", + "components.navigation.docs.links.guides": "Panduan", + "components.navigation.docs.links.dependencies": "Dependensi", + "components.navigation.getInvolved.links.collabSummit": "KKT Kolaborasi", + "components.navigation.getInvolved.links.contribute": "Kontribusi", + "components.navigation.getInvolved.links.codeOfConduct": "Kode etik", + "components.downloadList.links.previousReleases": "Rilisan sebelumnya", + "components.downloadList.links.packageManager": "Menginstal Node.js melalui manajer paket", + "components.downloadList.links.shaSums": "SHASUMS yang ditandatangani untuk file rilisan", + "components.downloadList.links.shaSums.howToVerify": " (Cara melakukan verifikasi)", + "components.downloadList.links.allDownloads": "Semua opsi unduhan", + "components.downloadList.links.nightlyReleases": "Build Harian", + "components.downloadList.links.unofficialBuilds": "Build tidak resmi", + "components.downloadList.links.buildingFromSource": "Membangun Node.js dari sumber pada platform yang didukung", + "components.downloadList.links.installingOnLinux": "Menginstal Node.js melalui arsip biner", + "components.downloadList.links.installingOnWsl": "Menginstal melalui Windows Subsystem for Linux (WSL)", + "components.downloadReleasesTable.changelog": "Log Perubahan", + "components.downloadReleasesTable.releases": "Rilisan", + "components.downloadReleasesTable.docs": "Dokumen", + "components.header.buttons.toggleLanguage": "Beralih Bahasa", + "components.header.buttons.toggleDarkMode": "Alihkan mode gelap/terang", + "components.pagination.next": "Terbaru | ", + "components.pagination.previous": "Lama", + "layouts.blogPost.author.byLine": "{author, select, null {} other {Oleh {author}, }}", + "layouts.blogIndex.currentYear": "Berita dari {year}", + "components.common.banner.button.text": "Baca Selengkapnya", + "components.article.author.githubLinkLabel": "{username} Github - buka di tab baru", + "components.article.authorList.title": "Penulis Artikel", + "components.article.editLink.title.edit": "Edit halaman ini di GitHub", + "components.article.editLink.title.translate": "Tertarik untuk membantu terjemahan?", + "components.common.languageSelector.button.title": "Ganti Bahasa", + "components.blog.blogCard.author.by": "oleh", + "components.sections.newFooter.trademark": "Kebijakan Merek Dagang", + "components.sections.newFooter.privacy": "Kebijakan Privasi", + "components.sections.newFooter.codeOfConduct": "Kode Etik", + "components.sections.newFooter.security": "Keamanan", + "components.sections.newFooter.about": "Tentang", + "components.sections.newFooter.blog": "Blog", + "components.sections.newFooter.github": "GitHub", + "components.learn.previousNextLink.next": "LANJUT", + "components.learn.previousNextLink.previous": "KEMBALI", + "components.home.nodeFeatures.javascript.title": "JavaScript", + "components.home.nodeFeatures.javascript.description": "Node.js menyediakan dukungan untuk bahasa pemrograman JavaScript", + "components.home.nodeFeatures.openSource.title": "Sumber Terbuka", + "components.home.nodeFeatures.openSource.description": "Node.js adalah open source dan dikelola secara aktif oleh kontributor di seluruh dunia", + "components.home.nodeFeatures.everywhere.title": "Dimana saja", + "components.home.nodeFeatures.everywhere.description": "Node.js telah diadaptasi untuk bekerja di berbagai tempat", + "components.common.shellBox.copy": "{copied, select, true {disalin}other {salin}}", + "components.codeBox.copy": "{copied, select, true {disalin}other {salin}}", + "components.api.stability": "Stabilitas: {level} - ", + "components.api.jsonLink.title": "Tampilkan sebagai JSON", + "components.api.sourceLink": "Kode Sumber:", + "components.sections.newHeader.links.learn": "Pelajari", + "components.sections.newHeader.links.apiDocs": "Dokumentasi API", + "components.sections.newHeader.links.about": "Tentang", + "components.sections.newHeader.links.download": "Unduh", + "components.sections.newHeader.links.certification": "Sertifikasi", + "components.downloads.downloadToggle.current": "Saat ini", + "components.downloads.downloadToggle.lts": "LTS", + "components.downloads.downloadToggle.recommendation": "{selected, select, LTS {Disarankan untuk banyak pengguna}other {Dengan fitur terbaru}}", + "components.downloads.downloadToggle.ltsVersions": "Versi LTS", + "components.downloads.downloadToggle.currentVersions": "Versi Saat Ini", + "pages.404.title": "404: Halaman tidak dapat ditemukan", + "pages.404.description": "ENOENT: tidak ada file atau direktori", + "components.api.apiChanges.addedIn": "Ditambahkan di: {version}", + "components.api.apiChanges.history": "Riwayat", + "components.api.apiChanges.history.version": "Versi", + "components.api.apiChanges.history.changes": "Perubahan" +} diff --git a/i18n/locales/index.mjs b/i18n/locales/index.mjs new file mode 100644 index 0000000000000..30f6e52a8dc5c --- /dev/null +++ b/i18n/locales/index.mjs @@ -0,0 +1,52 @@ +// Next.js React Intl Translation Manifest +// In order to add new Translations to this application, please add the new locale +// to the `i18n/config.json` file and then add the new locale file to this directory +// and import it below. + +import ar from './ar.json' assert { type: 'json' }; +import ca from './ca.json' assert { type: 'json' }; +import de from './de.json' assert { type: 'json' }; +import en from './en.json' assert { type: 'json' }; +import es from './es.json' assert { type: 'json' }; +import fa from './fa.json' assert { type: 'json' }; +import fr from './fr.json' assert { type: 'json' }; +import gl from './gl.json' assert { type: 'json' }; +import id from './id.json' assert { type: 'json' }; +import it from './it.json' assert { type: 'json' }; +import ja from './ja.json' assert { type: 'json' }; +import ka from './ka.json' assert { type: 'json' }; +import ko from './ko.json' assert { type: 'json' }; +import nl from './nl.json' assert { type: 'json' }; +import ptBr from './pt-br.json' assert { type: 'json' }; +import ro from './ro.json' assert { type: 'json' }; +import ru from './ru.json' assert { type: 'json' }; +import tr from './tr.json' assert { type: 'json' }; +import uk from './uk.json' assert { type: 'json' }; +import zhCn from './zh-cn.json' assert { type: 'json' }; +import zhTw from './zh-tw.json' assert { type: 'json' }; + +// This is the default export of the React Intl Locales and contains all the current locales +// eslint-disable-next-line import/no-anonymous-default-export +export default { + ar, + ca, + de, + en, + es, + fa, + fr, + gl, + id, + it, + ja, + ka, + ko, + nl, + 'pt-br': ptBr, + ro, + ru, + tr, + uk, + 'zh-cn': zhCn, + 'zh-tw': zhTw, +}; diff --git a/i18n/locales/it.json b/i18n/locales/it.json new file mode 100644 index 0000000000000..52620965c7a69 --- /dev/null +++ b/i18n/locales/it.json @@ -0,0 +1,39 @@ +{ + "components.footer.scrollToTop.button": "Portami all'inizio", + "components.header.links.home": "Home", + "components.header.links.about": "Informazioni", + "components.header.links.download": "Downloads", + "components.header.links.docs": "Documentazione", + "components.header.links.getInvolved": "Come partecipare", + "components.header.links.security": "Sicurezza", + "components.header.links.certification": "Certification", + "components.header.links.blog": "Blog", + "components.navigation.about.links.governance": "Gestione", + "components.navigation.docs.links.es6": "ES6 e oltre", + "components.navigation.docs.links.apiLts": "{fullLtsNodeVersion} API {spanLts}", + "components.navigation.docs.links.apiCurrent": "{fullCurrentNodeVersion} API", + "components.navigation.docs.links.guides": "Guide", + "components.navigation.docs.links.dependencies": "Dependencies", + "components.navigation.getInvolved.links.collabSummit": "Collab Summit", + "components.navigation.getInvolved.links.contribute": "Contribuire", + "components.navigation.getInvolved.links.codeOfConduct": "Condotta", + "components.downloadList.links.previousReleases": "Rilasci Precedenti", + "components.downloadList.links.packageManager": "Installa Node.js con un gestore di pacchetti", + "components.downloadList.links.shaSums": "Signed SHASUMS for release files", + "components.downloadList.links.shaSums.howToVerify": " (How to verify)", + "components.downloadList.links.allDownloads": "Tutti i download", + "components.downloadList.links.nightlyReleases": "Build notturne", + "components.downloadList.links.unofficialBuilds": "Unofficial builds", + "components.downloadList.links.buildingFromSource": "Building Node.js from source on supported platforms", + "components.downloadList.links.installingOnLinux": "Installing Node.js via binary archive", + "components.downloadList.links.installingOnWsl": "Install on Windows Subsystem for Linux (WSL)", + "components.downloadReleasesTable.changelog": "Registro modifiche", + "components.downloadReleasesTable.releases": "Releases", + "components.downloadReleasesTable.docs": "Documentazione", + "components.header.buttons.toggleLanguage": "Toggle Language", + "components.header.buttons.toggleDarkMode": "Toggle dark/light mode", + "components.pagination.next": "Successiva | ", + "components.pagination.previous": "Precedente", + "layouts.blogPost.author.byLine": "{author, select, null {} other {By {author}, }}", + "layouts.blogIndex.currentYear": "News from {year}" +} diff --git a/i18n/locales/ja.json b/i18n/locales/ja.json new file mode 100644 index 0000000000000..5f8f1ba9793ae --- /dev/null +++ b/i18n/locales/ja.json @@ -0,0 +1,39 @@ +{ + "components.footer.scrollToTop.button": "上部へスクロールする", + "components.header.links.home": "ホーム", + "components.header.links.about": "Node.js とは", + "components.header.links.download": "ダウンロード", + "components.header.links.docs": "ドキュメント", + "components.header.links.getInvolved": "参加する", + "components.header.links.security": "セキュリティ", + "components.header.links.certification": "Certification", + "components.header.links.blog": "ニュース", + "components.navigation.about.links.governance": "委員会", + "components.navigation.docs.links.es6": "ES6 について", + "components.navigation.docs.links.apiLts": "{fullLtsNodeVersion} API {spanLts}", + "components.navigation.docs.links.apiCurrent": "{fullCurrentNodeVersion} API", + "components.navigation.docs.links.guides": "ガイド", + "components.navigation.docs.links.dependencies": "依存関係", + "components.navigation.getInvolved.links.collabSummit": "Collab Summit", + "components.navigation.getInvolved.links.contribute": "貢献する", + "components.navigation.getInvolved.links.codeOfConduct": "行動規範", + "components.downloadList.links.previousReleases": "バージョンの一覧", + "components.downloadList.links.packageManager": "Iパッケージマネージャを使用したダウンロード", + "components.downloadList.links.shaSums": "リリースファイルのための SHASUM 署名", + "components.downloadList.links.shaSums.howToVerify": " (How to verify)", + "components.downloadList.links.allDownloads": "All download options", + "components.downloadList.links.nightlyReleases": "Nightly builds", + "components.downloadList.links.unofficialBuilds": "非公式のビルド版", + "components.downloadList.links.buildingFromSource": "Building Node.js from source on supported platforms", + "components.downloadList.links.installingOnLinux": "Installing Node.js via binary archive", + "components.downloadList.links.installingOnWsl": "Install on Windows Subsystem for Linux (WSL)", + "components.downloadReleasesTable.changelog": "変更履歴", + "components.downloadReleasesTable.releases": "Releases", + "components.downloadReleasesTable.docs": "ドキュメント", + "components.header.buttons.toggleLanguage": "Toggle Language", + "components.header.buttons.toggleDarkMode": "Toggle dark/light mode", + "components.pagination.next": "次 | ", + "components.pagination.previous": "前", + "layouts.blogPost.author.byLine": "{author, select, null {} other {By {author}, }}", + "layouts.blogIndex.currentYear": "News from {year}" +} diff --git a/i18n/locales/ka.json b/i18n/locales/ka.json new file mode 100644 index 0000000000000..e1b1797b00644 --- /dev/null +++ b/i18n/locales/ka.json @@ -0,0 +1,39 @@ +{ + "components.footer.scrollToTop.button": "ზემოთ", + "components.header.links.home": "მთავარი", + "components.header.links.about": "შესახებ", + "components.header.links.download": "ჩამოტვირთვა", + "components.header.links.docs": "დოკუმენტაცია", + "components.header.links.getInvolved": "შემოგვიერთდით", + "components.header.links.security": "უსაფრთხოება", + "components.header.links.certification": "სერტიფიკაცია", + "components.header.links.blog": "სიახლეები", + "components.navigation.about.links.governance": "მმართველობა", + "components.navigation.docs.links.es6": "ES6 და მის ფარგლებს მიღმა", + "components.navigation.docs.links.apiLts": "{fullLtsNodeVersion} API {spanLts}", + "components.navigation.docs.links.apiCurrent": "{fullCurrentNodeVersion} API", + "components.navigation.docs.links.guides": "სახელმძღვანელოები", + "components.navigation.docs.links.dependencies": "დაქვემდებარებულები (Dependencies)", + "components.navigation.getInvolved.links.collabSummit": "თანამშრომლობის სამიტი", + "components.navigation.getInvolved.links.contribute": "ხელშეწყობა", + "components.navigation.getInvolved.links.codeOfConduct": "ყოფაქცევის კოდექსი", + "components.downloadList.links.previousReleases": "უწინდელი ვერსიები", + "components.downloadList.links.packageManager": "Node.js-ის ინსტალაცია პაკეტის მენეჯერის გამოყენებით", + "components.downloadList.links.shaSums": "ხელმოწერილი SHASUMS გამოშვების (release) ფაილებისათვის", + "components.downloadList.links.shaSums.howToVerify": " (როგორ გადავამოწმოთ)", + "components.downloadList.links.allDownloads": "ჩამოტვირთვის ყველა ვარიანტი", + "components.downloadList.links.nightlyReleases": "ღამის ვერსიები", + "components.downloadList.links.unofficialBuilds": "არაოფიციალური ვერსიები", + "components.downloadList.links.buildingFromSource": "Building Node.js from source on supported platforms", + "components.downloadList.links.installingOnLinux": "Node.js-ის ინსტალაცია ბინარული (binary) არქივის გამოყენებით", + "components.downloadList.links.installingOnWsl": "ინსტალაცია Windows-ის Linux-ქვესისტემისათვის (WSL)", + "components.downloadReleasesTable.changelog": "ცვლილებათა ჩამონათვალი", + "components.downloadReleasesTable.releases": "უწინდელი ვერსიები", + "components.downloadReleasesTable.docs": "დოკუმენტაცია", + "components.header.buttons.toggleLanguage": "Toggle Language", + "components.header.buttons.toggleDarkMode": "გადართვა მუქ/ნათელ რეჟიმზე", + "components.pagination.next": "შემდეგი | ", + "components.pagination.previous": "წინა", + "layouts.blogPost.author.byLine": "{author, select, null {} other {By {author}, }}", + "layouts.blogIndex.currentYear": "News from {year}" +} diff --git a/i18n/locales/ko.json b/i18n/locales/ko.json new file mode 100644 index 0000000000000..a582789464260 --- /dev/null +++ b/i18n/locales/ko.json @@ -0,0 +1,39 @@ +{ + "components.footer.scrollToTop.button": "맨 위로", + "components.header.links.home": "홈", + "components.header.links.about": "About", + "components.header.links.download": "다운로드", + "components.header.links.docs": "문서", + "components.header.links.getInvolved": "참여하기", + "components.header.links.security": "보안", + "components.header.links.certification": "Certification", + "components.header.links.blog": "뉴스", + "components.navigation.about.links.governance": "거버넌스", + "components.navigation.docs.links.es6": "ES6와 그 이후", + "components.navigation.docs.links.apiLts": "{fullLtsNodeVersion} API {spanLts}", + "components.navigation.docs.links.apiCurrent": "{fullCurrentNodeVersion} API", + "components.navigation.docs.links.guides": "안내", + "components.navigation.docs.links.dependencies": "의존성", + "components.navigation.getInvolved.links.collabSummit": "협업자 서밋", + "components.navigation.getInvolved.links.contribute": "기여하기", + "components.navigation.getInvolved.links.codeOfConduct": "행동강령", + "components.downloadList.links.previousReleases": "이전 릴리스", + "components.downloadList.links.packageManager": "패키지 관리자를 통한 Node.js 설치", + "components.downloadList.links.shaSums": "릴리스 파일에 서명된 SHASUMS", + "components.downloadList.links.shaSums.howToVerify": " (검증 방법)", + "components.downloadList.links.allDownloads": "모든 다운로드 보기", + "components.downloadList.links.nightlyReleases": "나이틀리 빌드", + "components.downloadList.links.unofficialBuilds": "비공식 빌드", + "components.downloadList.links.buildingFromSource": "Building Node.js from source on supported platforms", + "components.downloadList.links.installingOnLinux": "Linux에서 바이너리 아카이브를 통해 Node.js 설치하기", + "components.downloadList.links.installingOnWsl": "Install on Windows Subsystem for Linux (WSL)", + "components.downloadReleasesTable.changelog": "변경사항", + "components.downloadReleasesTable.releases": "Releases", + "components.downloadReleasesTable.docs": "문서", + "components.header.buttons.toggleLanguage": "Toggle Language", + "components.header.buttons.toggleDarkMode": "Toggle dark/light mode", + "components.pagination.next": "다음 | ", + "components.pagination.previous": "이전", + "layouts.blogPost.author.byLine": "{author, select, null {} other {By {author}, }}", + "layouts.blogIndex.currentYear": "News from {year}" +} diff --git a/i18n/locales/nl.json b/i18n/locales/nl.json new file mode 100644 index 0000000000000..bf2f17011d92e --- /dev/null +++ b/i18n/locales/nl.json @@ -0,0 +1,39 @@ +{ + "components.footer.scrollToTop.button": "Scroll naar boven", + "components.header.links.home": "Home", + "components.header.links.about": "Over Node.js", + "components.header.links.download": "Downloads", + "components.header.links.docs": "Documentatie", + "components.header.links.getInvolved": "Raak Betrokken", + "components.header.links.security": "Security", + "components.header.links.certification": "Certificering", + "components.header.links.blog": "Nieuws", + "components.navigation.about.links.governance": "Bestuur", + "components.navigation.docs.links.es6": "ES6 en meer", + "components.navigation.docs.links.apiLts": "{fullLtsNodeVersion} API {spanLts}", + "components.navigation.docs.links.apiCurrent": "{fullCurrentNodeVersion} API", + "components.navigation.docs.links.guides": "Handleidingen", + "components.navigation.docs.links.dependencies": "Dependencies", + "components.navigation.getInvolved.links.collabSummit": "Collab Summit", + "components.navigation.getInvolved.links.contribute": "Bijdragen", + "components.navigation.getInvolved.links.codeOfConduct": "Code of Conduct", + "components.downloadList.links.previousReleases": "Vorige Versies", + "components.downloadList.links.packageManager": "Installeer Node.js via package manager", + "components.downloadList.links.shaSums": "Ondertekende SHASUMS voor release bestanden", + "components.downloadList.links.shaSums.howToVerify": " (Hoe verifiëren?)", + "components.downloadList.links.allDownloads": "Alle download opties", + "components.downloadList.links.nightlyReleases": "Nightly builds", + "components.downloadList.links.unofficialBuilds": "Onofficiële builds", + "components.downloadList.links.buildingFromSource": "Building Node.js from source on supported platforms", + "components.downloadList.links.installingOnLinux": "Installeer Node.js via binary archive", + "components.downloadList.links.installingOnWsl": "Installeer op Windows Subsystem voor Linux (WSL)", + "components.downloadReleasesTable.changelog": "Changelog", + "components.downloadReleasesTable.releases": "Releases", + "components.downloadReleasesTable.docs": "Documentatie", + "components.header.buttons.toggleLanguage": "Toggle Language", + "components.header.buttons.toggleDarkMode": "Toggle dark/light mode", + "components.pagination.next": "Volgende | ", + "components.pagination.previous": "Vorige", + "layouts.blogPost.author.byLine": "{author, select, null {} other {By {author}, }}", + "layouts.blogIndex.currentYear": "News from {year}" +} diff --git a/i18n/locales/pt-br.json b/i18n/locales/pt-br.json new file mode 100644 index 0000000000000..1475f76f1aea4 --- /dev/null +++ b/i18n/locales/pt-br.json @@ -0,0 +1,39 @@ +{ + "components.footer.scrollToTop.button": "Retorne ao começo", + "components.header.links.home": "Início", + "components.header.links.about": "Sobre", + "components.header.links.download": "Download", + "components.header.links.docs": "Documentação", + "components.header.links.getInvolved": "Participe", + "components.header.links.security": "Segurança", + "components.header.links.certification": "Certification", + "components.header.links.blog": "Novidades", + "components.navigation.about.links.governance": "Governança do Projeto", + "components.navigation.docs.links.es6": "ES6 e além", + "components.navigation.docs.links.apiLts": "{fullLtsNodeVersion} API {spanLts}", + "components.navigation.docs.links.apiCurrent": "{fullCurrentNodeVersion} API", + "components.navigation.docs.links.guides": "Guias", + "components.navigation.docs.links.dependencies": "Dependências", + "components.navigation.getInvolved.links.collabSummit": "Collab Summit", + "components.navigation.getInvolved.links.contribute": "Contribua", + "components.navigation.getInvolved.links.codeOfConduct": "Código de Conduta", + "components.downloadList.links.previousReleases": "Versões Anteriores", + "components.downloadList.links.packageManager": "Instale Node.js usando gerenciador de pacotes.", + "components.downloadList.links.shaSums": "SHASUMS assinados para arquivos de versões", + "components.downloadList.links.shaSums.howToVerify": " (Como verificar)", + "components.downloadList.links.allDownloads": "Todas as opções de download", + "components.downloadList.links.nightlyReleases": "Versões Nightly", + "components.downloadList.links.unofficialBuilds": "Unofficial builds", + "components.downloadList.links.buildingFromSource": "Building Node.js from source on supported platforms", + "components.downloadList.links.installingOnLinux": "Instalando Node.js via arquivo binário.", + "components.downloadList.links.installingOnWsl": "Install on Windows Subsystem for Linux (WSL)", + "components.downloadReleasesTable.changelog": "Changelog", + "components.downloadReleasesTable.releases": "Releases", + "components.downloadReleasesTable.docs": "Documentação", + "components.header.buttons.toggleLanguage": "Toggle Language", + "components.header.buttons.toggleDarkMode": "Toggle dark/light mode", + "components.pagination.next": "Próximo | ", + "components.pagination.previous": "Anterior", + "layouts.blogPost.author.byLine": "{author, select, null {} other {By {author}, }}", + "layouts.blogIndex.currentYear": "News from {year}" +} diff --git a/i18n/locales/ro.json b/i18n/locales/ro.json new file mode 100644 index 0000000000000..0f671085c8d58 --- /dev/null +++ b/i18n/locales/ro.json @@ -0,0 +1,39 @@ +{ + "components.footer.scrollToTop.button": "Scroll to top", + "components.header.links.home": "Home", + "components.header.links.about": "About", + "components.header.links.download": "Downloads", + "components.header.links.docs": "Docs", + "components.header.links.getInvolved": "Get Involved", + "components.header.links.security": "Security", + "components.header.links.certification": "Certification", + "components.header.links.blog": "News", + "components.navigation.about.links.governance": "Governance", + "components.navigation.docs.links.es6": "ES6 and beyond", + "components.navigation.docs.links.apiLts": "{fullLtsNodeVersion} API {spanLts}", + "components.navigation.docs.links.apiCurrent": "{fullCurrentNodeVersion} API", + "components.navigation.docs.links.guides": "Guides", + "components.navigation.docs.links.dependencies": "Dependencies", + "components.navigation.getInvolved.links.collabSummit": "Collab Summit", + "components.navigation.getInvolved.links.contribute": "Contribute", + "components.navigation.getInvolved.links.codeOfConduct": "Code of Conduct", + "components.downloadList.links.previousReleases": "Previous Releases", + "components.downloadList.links.packageManager": "Installing Node.js via package manager", + "components.downloadList.links.shaSums": "Signed SHASUMS for release files", + "components.downloadList.links.shaSums.howToVerify": " (How to verify)", + "components.downloadList.links.allDownloads": "All download options", + "components.downloadList.links.nightlyReleases": "Nightly builds", + "components.downloadList.links.unofficialBuilds": "Unofficial builds", + "components.downloadList.links.buildingFromSource": "Building Node.js from source on supported platforms", + "components.downloadList.links.installingOnLinux": "Installing Node.js via binary archive", + "components.downloadList.links.installingOnWsl": "Install on Windows Subsystem for Linux (WSL)", + "components.downloadReleasesTable.changelog": "Changelog", + "components.downloadReleasesTable.releases": "Releases", + "components.downloadReleasesTable.docs": "Docs", + "components.header.buttons.toggleLanguage": "Toggle Language", + "components.header.buttons.toggleDarkMode": "Toggle dark/light mode", + "components.pagination.next": "Newer | ", + "components.pagination.previous": "Older", + "layouts.blogPost.author.byLine": "{author, select, null {} other {By {author}, }}", + "layouts.blogIndex.currentYear": "News from {year}" +} diff --git a/i18n/locales/ru.json b/i18n/locales/ru.json new file mode 100644 index 0000000000000..558d177412467 --- /dev/null +++ b/i18n/locales/ru.json @@ -0,0 +1,39 @@ +{ + "components.footer.scrollToTop.button": "Вверх", + "components.header.links.home": "Главная", + "components.header.links.about": "О проекте", + "components.header.links.download": "Загрузка", + "components.header.links.docs": "Документация", + "components.header.links.getInvolved": "Присоединиться", + "components.header.links.security": "Безопасность", + "components.header.links.certification": "Certification", + "components.header.links.blog": "Новости", + "components.navigation.about.links.governance": "Управление", + "components.navigation.docs.links.es6": "ES6 и выше", + "components.navigation.docs.links.apiLts": "{fullLtsNodeVersion} API {spanLts}", + "components.navigation.docs.links.apiCurrent": "{fullCurrentNodeVersion} API", + "components.navigation.docs.links.guides": "Руководства", + "components.navigation.docs.links.dependencies": "Зависимости", + "components.navigation.getInvolved.links.collabSummit": "Саммит сотрудников", + "components.navigation.getInvolved.links.contribute": "Участие", + "components.navigation.getInvolved.links.codeOfConduct": "Правила", + "components.downloadList.links.previousReleases": "Предыдущие релизы", + "components.downloadList.links.packageManager": "Установка Node.js с помощью пакетного менеджера", + "components.downloadList.links.shaSums": "Подписанные SHASUMS для файлов релиза", + "components.downloadList.links.shaSums.howToVerify": " (Как проверить)", + "components.downloadList.links.allDownloads": "Все варианты загрузки", + "components.downloadList.links.nightlyReleases": "Ночные сборки", + "components.downloadList.links.unofficialBuilds": "Неофициальные сборки", + "components.downloadList.links.buildingFromSource": "Building Node.js from source on supported platforms", + "components.downloadList.links.installingOnLinux": "Установка Node.js через бинарный архив в Linux", + "components.downloadList.links.installingOnWsl": "Install on Windows Subsystem for Linux (WSL)", + "components.downloadReleasesTable.changelog": "Список изменений", + "components.downloadReleasesTable.releases": "Releases", + "components.downloadReleasesTable.docs": "Документация", + "components.header.buttons.toggleLanguage": "Toggle Language", + "components.header.buttons.toggleDarkMode": "Toggle dark/light mode", + "components.pagination.next": "Следующий | ", + "components.pagination.previous": "Предыдущий", + "layouts.blogPost.author.byLine": "{author, select, null {} other {By {author}, }}", + "layouts.blogIndex.currentYear": "News from {year}" +} diff --git a/i18n/locales/tr.json b/i18n/locales/tr.json new file mode 100644 index 0000000000000..81994747bc8b6 --- /dev/null +++ b/i18n/locales/tr.json @@ -0,0 +1,39 @@ +{ + "components.footer.scrollToTop.button": "En üste kaydır", + "components.header.links.home": "Anasayfa", + "components.header.links.about": "Hakkında", + "components.header.links.download": "İndir", + "components.header.links.docs": "Belgeler", + "components.header.links.getInvolved": "Dahil Olun", + "components.header.links.security": "Güvenlik", + "components.header.links.certification": "Sertifikasyon", + "components.header.links.blog": "Haberler", + "components.navigation.about.links.governance": "Yönetim Şekli", + "components.navigation.docs.links.es6": "ES6 ve sonrası", + "components.navigation.docs.links.apiLts": "{fullLtsNodeVersion} API {spanLts}", + "components.navigation.docs.links.apiCurrent": "{fullCurrentNodeVersion} API", + "components.navigation.docs.links.guides": "Rehberler", + "components.navigation.docs.links.dependencies": "Kullanılan Kütüphaneler", + "components.navigation.getInvolved.links.collabSummit": "İşbirliği Zirvesi", + "components.navigation.getInvolved.links.contribute": "Katkıda bulunun", + "components.navigation.getInvolved.links.codeOfConduct": "Davranış Kuralları", + "components.downloadList.links.previousReleases": "Önceki Sürümler", + "components.downloadList.links.packageManager": "Node.js'i paket yöneticisi ile kurmak", + "components.downloadList.links.shaSums": "Sürüm dosyaları için imzalı SHASUMS", + "components.downloadList.links.shaSums.howToVerify": " (Nasıl doğrulanır)", + "components.downloadList.links.allDownloads": "Tüm indirme seçenekleri", + "components.downloadList.links.nightlyReleases": "Günlük Yayımlanan Sürümler", + "components.downloadList.links.unofficialBuilds": "Resmi olmayan sürümler", + "components.downloadList.links.buildingFromSource": "Desteklenen platformlarda Node.js'in kaynak kodundan derlenmesi", + "components.downloadList.links.installingOnLinux": "Binary arşiv yoluyla Node.js kurulumu", + "components.downloadList.links.installingOnWsl": "Linux için Windows Subsystem'a (WSL) kurun", + "components.downloadReleasesTable.changelog": "Değişiklikler", + "components.downloadReleasesTable.releases": "Sürümler", + "components.downloadReleasesTable.docs": "Dokümantasyon", + "components.header.buttons.toggleLanguage": "Dili Değiştir", + "components.header.buttons.toggleDarkMode": "Karanlık/aydınlık modunu değiştir", + "components.pagination.next": "Daha Yeni | ", + "components.pagination.previous": "Daha Eski", + "layouts.blogPost.author.byLine": "{author, select, null {} other {{author} tarafından }}", + "layouts.blogIndex.currentYear": "{year} yılından haberler" +} diff --git a/i18n/locales/uk.json b/i18n/locales/uk.json new file mode 100644 index 0000000000000..ca1f69103c700 --- /dev/null +++ b/i18n/locales/uk.json @@ -0,0 +1,39 @@ +{ + "components.footer.scrollToTop.button": "Вгору", + "components.header.links.home": "Головна", + "components.header.links.about": "Про проект", + "components.header.links.download": "Завантаження", + "components.header.links.docs": "Документація", + "components.header.links.getInvolved": "Приєднатись", + "components.header.links.security": "Безпека", + "components.header.links.certification": "Certification", + "components.header.links.blog": "Новини", + "components.navigation.about.links.governance": "Управління", + "components.navigation.docs.links.es6": "ES6 і вище", + "components.navigation.docs.links.apiLts": "{fullLtsNodeVersion} API {spanLts}", + "components.navigation.docs.links.apiCurrent": "{fullCurrentNodeVersion} API", + "components.navigation.docs.links.guides": "Керівництва", + "components.navigation.docs.links.dependencies": "Залежності", + "components.navigation.getInvolved.links.collabSummit": "Саміт співпраці", + "components.navigation.getInvolved.links.contribute": "Сприяння", + "components.navigation.getInvolved.links.codeOfConduct": "Правила", + "components.downloadList.links.previousReleases": "Попередні Релізи", + "components.downloadList.links.packageManager": "Встановлення Node.js через пакетний менеджер", + "components.downloadList.links.shaSums": "Підписані SHASUMS для файлів релізу", + "components.downloadList.links.shaSums.howToVerify": " (Як перевірити)", + "components.downloadList.links.allDownloads": "Всі варіанти завантажень", + "components.downloadList.links.nightlyReleases": "Нічні збірки", + "components.downloadList.links.unofficialBuilds": "Unofficial builds", + "components.downloadList.links.buildingFromSource": "Building Node.js from source on supported platforms", + "components.downloadList.links.installingOnLinux": "Installing Node.js via binary archive", + "components.downloadList.links.installingOnWsl": "Install on Windows Subsystem for Linux (WSL)", + "components.downloadReleasesTable.changelog": "Список змін", + "components.downloadReleasesTable.releases": "Releases", + "components.downloadReleasesTable.docs": "Документація", + "components.header.buttons.toggleLanguage": "Toggle Language", + "components.header.buttons.toggleDarkMode": "Toggle dark/light mode", + "components.pagination.next": "Далі | ", + "components.pagination.previous": "Попередній", + "layouts.blogPost.author.byLine": "{author, select, null {} other {By {author}, }}", + "layouts.blogIndex.currentYear": "News from {year}" +} diff --git a/i18n/locales/zh-cn.json b/i18n/locales/zh-cn.json new file mode 100644 index 0000000000000..6f0f8886e4f3f --- /dev/null +++ b/i18n/locales/zh-cn.json @@ -0,0 +1,82 @@ +{ + "components.footer.scrollToTop.button": "回到页顶", + "components.header.links.home": "首页", + "components.header.links.about": "关于", + "components.header.links.download": "下载", + "components.header.links.docs": "文档", + "components.header.links.getInvolved": "加入我们", + "components.header.links.security": "汇报安全漏洞", + "components.header.links.certification": "相关认证", + "components.header.links.blog": "新闻事件", + "components.navigation.about.links.governance": "管理规则", + "components.navigation.docs.links.es6": "ES6 及更高版本", + "components.navigation.docs.links.apiLts": "{fullLtsNodeVersion} API {spanLts}", + "components.navigation.docs.links.apiCurrent": "{fullCurrentNodeVersion} API", + "components.navigation.docs.links.guides": "指南", + "components.navigation.docs.links.dependencies": "依赖项", + "components.navigation.getInvolved.links.collabSummit": "协作者峰会", + "components.navigation.getInvolved.links.contribute": "贡献", + "components.navigation.getInvolved.links.codeOfConduct": "管理", + "components.downloadList.links.previousReleases": "先前所有已发布版本", + "components.downloadList.links.packageManager": "使用包管理器安装 Node.js", + "components.downloadList.links.shaSums": "查看发布文件的 SHASUM 签名", + "components.downloadList.links.shaSums.howToVerify": " (如何校验?)", + "components.downloadList.links.allDownloads": "所有下载选项", + "components.downloadList.links.nightlyReleases": "日构建版(仅供实验尝鲜用)", + "components.downloadList.links.unofficialBuilds": "非官方构建版", + "components.downloadList.links.buildingFromSource": "在已支持平台上通过编译 Node.js 源码得到可执行文件的步骤", + "components.downloadList.links.installingOnLinux": "在 Linux 上,通过二进制文件安装 Node.js", + "components.downloadList.links.installingOnWsl": "在适用于 Linux 的 Windows 子系统(WSL)上安装 Node.js", + "components.downloadReleasesTable.changelog": "更新日志", + "components.downloadReleasesTable.releases": "查看与下载", + "components.downloadReleasesTable.docs": "文档", + "components.header.buttons.toggleLanguage": "切换语言", + "components.header.buttons.toggleDarkMode": "明亮/暗黑模式切换", + "components.pagination.next": "较新的新闻事件 | ", + "components.pagination.previous": "更早的新闻事件", + "layouts.blogPost.author.byLine": "{author, select, null {} other {由 {author} }}", + "layouts.blogIndex.currentYear": "{year} 年的所有新闻事件", + "components.common.banner.button.text": "了解更多……", + "components.article.author.githubLinkLabel": "{username} Github - 在新标签中打开", + "components.article.authorList.title": "文章作者", + "components.article.editLink.title.edit": "在 GitHub 上编辑此页面", + "components.article.editLink.title.translate": "您愿意帮助我们翻译吗?", + "components.common.languageSelector.button.title": "切换语言", + "components.blog.blogCard.author.by": "由", + "components.sections.newFooter.trademark": "商标政策", + "components.sections.newFooter.privacy": "隐私政策", + "components.sections.newFooter.codeOfConduct": "行为守则", + "components.sections.newFooter.security": "汇报安全漏洞", + "components.sections.newFooter.about": "关于我们", + "components.sections.newFooter.blog": "博客", + "components.sections.newFooter.github": "GitHub", + "components.learn.previousNextLink.next": "下一页", + "components.learn.previousNextLink.previous": "上一页", + "components.home.nodeFeatures.javascript.title": "JavaScript", + "components.home.nodeFeatures.javascript.description": "Node.js 支持 JavaScript 编程语言", + "components.home.nodeFeatures.openSource.title": "自由开源", + "components.home.nodeFeatures.openSource.description": "Node.js是开源的,并由世界各地的贡献者积极维护。", + "components.home.nodeFeatures.everywhere.title": "全球", + "components.home.nodeFeatures.everywhere.description": "Node.js 已经过调整,可以适配在许多不同的场景下工作", + "components.common.shellBox.copy": "{copied, select, true {复制了}other {复制}}", + "components.codeBox.copy": "{copied, select, true {复制了}other {复制}}", + "components.api.stability": "稳定性级别: {level} - ", + "components.api.jsonLink.title": "以 JSON 格式查看", + "components.api.sourceLink": "源代码:", + "components.sections.newHeader.links.learn": "学习", + "components.sections.newHeader.links.apiDocs": "API 文档", + "components.sections.newHeader.links.about": "关于 Node.js", + "components.sections.newHeader.links.download": "下载", + "components.sections.newHeader.links.certification": "认证", + "components.downloads.downloadToggle.current": "最新尝鲜版", + "components.downloads.downloadToggle.lts": "长期维护版", + "components.downloads.downloadToggle.recommendation": "{selected, select, LTS {推荐大多数用户}other {使用最新功能}}", + "components.downloads.downloadToggle.ltsVersions": "LTS 版本", + "components.downloads.downloadToggle.currentVersions": "最新尝鲜版本", + "pages.404.title": "404:您访问的页面不存在", + "pages.404.description": "ENOENT:无此文件或目录", + "components.api.apiChanges.addedIn": "自 {version} 开始添加", + "components.api.apiChanges.history": "历史记录", + "components.api.apiChanges.history.version": "版本号", + "components.api.apiChanges.history.changes": "变更记录" +} diff --git a/i18n/locales/zh-tw.json b/i18n/locales/zh-tw.json new file mode 100644 index 0000000000000..cc5632a00f516 --- /dev/null +++ b/i18n/locales/zh-tw.json @@ -0,0 +1,39 @@ +{ + "components.footer.scrollToTop.button": "回到頁首", + "components.header.links.home": "首頁", + "components.header.links.about": "關於我們", + "components.header.links.download": "下載", + "components.header.links.docs": "文件", + "components.header.links.getInvolved": "加入我們", + "components.header.links.security": "安全", + "components.header.links.certification": "相關認證", + "components.header.links.blog": "部落格", + "components.navigation.about.links.governance": "管理規則", + "components.navigation.docs.links.es6": "ES6 相關", + "components.navigation.docs.links.apiLts": "{fullLtsNodeVersion} API {spanLts}", + "components.navigation.docs.links.apiCurrent": "{fullCurrentNodeVersion} API", + "components.navigation.docs.links.guides": "技術指南", + "components.navigation.docs.links.dependencies": "Dependencies", + "components.navigation.getInvolved.links.collabSummit": "協作者峰會", + "components.navigation.getInvolved.links.contribute": "貢獻", + "components.navigation.getInvolved.links.codeOfConduct": "管理", + "components.downloadList.links.previousReleases": "舊版", + "components.downloadList.links.packageManager": "I使用套件管理器安裝 Node.js", + "components.downloadList.links.shaSums": "發佈檔案的 SHASUM 簽名", + "components.downloadList.links.shaSums.howToVerify": " (How to verify)", + "components.downloadList.links.allDownloads": "所有下載選項", + "components.downloadList.links.nightlyReleases": "每日建置版本", + "components.downloadList.links.unofficialBuilds": "非官方建置版本", + "components.downloadList.links.buildingFromSource": "Building Node.js from source on supported platforms", + "components.downloadList.links.installingOnLinux": "在 Linux 上使用二進位檔案安裝 Node.js", + "components.downloadList.links.installingOnWsl": "Install on Windows Subsystem for Linux (WSL)", + "components.downloadReleasesTable.changelog": "更新紀錄", + "components.downloadReleasesTable.releases": "Releases", + "components.downloadReleasesTable.docs": "文件", + "components.header.buttons.toggleLanguage": "Toggle Language", + "components.header.buttons.toggleDarkMode": "Toggle dark/light mode", + "components.pagination.next": "下一個 | ", + "components.pagination.previous": "上一個", + "layouts.blogPost.author.byLine": "{author, select, null {} other {By {author}, }}", + "layouts.blogIndex.currentYear": "News from {year}" +} diff --git a/jest.config.mjs b/jest.config.mjs new file mode 100644 index 0000000000000..027f6dc79b9c0 --- /dev/null +++ b/jest.config.mjs @@ -0,0 +1,15 @@ +import nextJest from 'next/jest.js'; + +const createJestConfig = nextJest({ + dir: './', +}); + +/** @type {import('jest').Config} */ +const customJestConfig = { + setupFilesAfterEnv: ['/jest.setup.mjs'], + moduleDirectories: ['node_modules', '/'], + testEnvironment: 'jest-environment-jsdom', + testMatch: ['**/__tests__/*.test.{,ts,tsx}'], +}; + +export default createJestConfig(customJestConfig); diff --git a/jest.setup.mjs b/jest.setup.mjs new file mode 100644 index 0000000000000..666127af390a1 --- /dev/null +++ b/jest.setup.mjs @@ -0,0 +1 @@ +import '@testing-library/jest-dom/extend-expect'; diff --git a/layouts/AboutLayout.tsx b/layouts/AboutLayout.tsx new file mode 100644 index 0000000000000..2a4a0b23dc557 --- /dev/null +++ b/layouts/AboutLayout.tsx @@ -0,0 +1,14 @@ +import BaseLayout from './BaseLayout'; +import SideNavigation from '../components/SideNavigation'; +import type { FC, PropsWithChildren } from 'react'; + +const AboutLayout: FC = ({ children }) => ( + +
    + +
    {children}
    +
    +
    +); + +export default AboutLayout; diff --git a/layouts/BaseLayout.tsx b/layouts/BaseLayout.tsx new file mode 100644 index 0000000000000..64e7476155e33 --- /dev/null +++ b/layouts/BaseLayout.tsx @@ -0,0 +1,13 @@ +import Footer from '../components/Footer'; +import Header from '../components/Header'; +import type { FC, PropsWithChildren } from 'react'; + +const BaseLayout: FC = ({ children }) => ( + <> +
    +
    {children}
    +