diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 92e2f07fbc..ab01c1e81d 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -81,6 +81,15 @@ jobs: - name: Install Netlify CLI run: npm install netlify-cli -g + - name: Set branch context URL + run: | + if [[ ! -z "${{ inputs.netlify-alias }}" ]]; then + netlify env:set NEXTAUTH_URL ${{ inputs.site-url || vars.SITE_URL }} --scope functions --context "branch:${{ inputs.netlify-alias }}" + fi + env: + NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} + NETLIFY_SITE_ID: ${{ vars.NETLIFY_SITE_ID }} + - name: Build using Netlify run: netlify build --context ${{ inputs.netlify-context }} --offline working-directory: site/gatsby-site @@ -95,11 +104,9 @@ jobs: GATSBY_ALGOLIA_SEARCH_KEY: ${{ vars.GATSBY_ALGOLIA_SEARCH_KEY }} GATSBY_AVAILABLE_LANGUAGES: ${{ vars.GATSBY_AVAILABLE_LANGUAGES }} GATSBY_REALM_APP_ID: ${{ vars.GATSBY_REALM_APP_ID }} - GOOGLE_TRANSLATE_API_KEY: ${{ secrets.GOOGLE_TRANSLATE_API_KEY }} MONGODB_CONNECTION_STRING: ${{ secrets.MONGODB_CONNECTION_STRING }} MONGODB_REPLICA_SET: ${{ secrets.MONGODB_REPLICA_SET }} MONGODB_TRANSLATIONS_CONNECTION_STRING: ${{ secrets.MONGODB_TRANSLATIONS_CONNECTION_STRING }} - TRANSLATE_SUBMISSION_DATE_START: ${{ vars.TRANSLATE_SUBMISSION_DATE_START }} MONGODB_MIGRATIONS_CONNECTION_STRING: ${{ secrets.MONGODB_MIGRATIONS_CONNECTION_STRING }} GATSBY_REALM_APP_GRAPHQL_URL: ${{ secrets.GATSBY_REALM_APP_GRAPHQL_URL }} GATSBY_PRISMIC_REPO_NAME: ${{ vars.GATSBY_PRISMIC_REPO_NAME }} @@ -114,7 +121,6 @@ jobs: CLOUDFLARE_R2_SECRET_ACCESS_KEY: ${{ secrets.CLOUDFLARE_R2_SECRET_ACCESS_KEY }} REALM_GRAPHQL_API_KEY: ${{ secrets.REALM_GRAPHQL_API_KEY }} GATSBY_COMMIT_SHA: ${{ inputs.sha }} - TRANSLATE_DRY_RUN: ${{ vars.TRANSLATE_DRY_RUN }} REALM_API_APP_ID: ${{ vars.REALM_API_APP_ID }} REALM_API_GROUP_ID: ${{ vars.REALM_API_GROUP_ID }} REALM_APP_ID: ${{ vars.GATSBY_REALM_APP_ID }} @@ -123,6 +129,8 @@ jobs: ROLLBAR_POST_SERVER_ITEM_ACCESS_TOKEN: ${{ secrets.GATSBY_ROLLBAR_TOKEN }} API_MONGODB_CONNECTION_STRING: ${{ secrets.API_MONGODB_CONNECTION_STRING }} SITE_URL: ${{ inputs.site-url || vars.SITE_URL }} + NEXTAUTH_URL: ${{ inputs.site-url || vars.SITE_URL }} + NEXTAUTH_SECRET: ${{ secrets.NEXTAUTH_SECRET }} - name: Build size run: | diff --git a/.github/workflows/test-api.yml b/.github/workflows/test-api.yml index 25d95b0440..12df1bfa15 100644 --- a/.github/workflows/test-api.yml +++ b/.github/workflows/test-api.yml @@ -70,6 +70,8 @@ jobs: NOTIFICATIONS_SENDER_NAME: Test Preview NOTIFICATIONS_SENDER: test@test.com SITE_URL: http://localhost:8000 + NEXTAUTH_URL: http://localhost:8000 + NEXTAUTH_SECRET: dummy - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v4 diff --git a/.github/workflows/test-build.yml b/.github/workflows/test-build.yml index 143beefab6..a59585c4e8 100644 --- a/.github/workflows/test-build.yml +++ b/.github/workflows/test-build.yml @@ -83,12 +83,10 @@ jobs: GATSBY_ALGOLIA_SEARCH_KEY: ${{ vars.GATSBY_ALGOLIA_SEARCH_KEY }} GATSBY_AVAILABLE_LANGUAGES: ${{ vars.GATSBY_AVAILABLE_LANGUAGES }} GATSBY_REALM_APP_ID: ${{ vars.GATSBY_REALM_APP_ID }} - GOOGLE_TRANSLATE_API_KEY: ${{ secrets.GOOGLE_TRANSLATE_API_KEY }} MONGODB_CONNECTION_STRING: ${{ secrets.MONGODB_CONNECTION_STRING }} MONGODB_REPLICA_SET: ${{ secrets.MONGODB_REPLICA_SET }} MONGODB_TRANSLATIONS_CONNECTION_STRING: ${{ secrets.MONGODB_TRANSLATIONS_CONNECTION_STRING }} MONGODB_MIGRATIONS_CONNECTION_STRING: ${{ secrets.MONGODB_MIGRATIONS_CONNECTION_STRING }} - TRANSLATE_SUBMISSION_DATE_START: ${{ vars.TRANSLATE_SUBMISSION_DATE_START }} GATSBY_REALM_APP_GRAPHQL_URL: ${{ secrets.GATSBY_REALM_APP_GRAPHQL_URL }} GATSBY_PRISMIC_REPO_NAME: ${{ vars.GATSBY_PRISMIC_REPO_NAME }} PRISMIC_ACCESS_TOKEN: ${{ secrets.PRISMIC_ACCESS_TOKEN }} @@ -109,6 +107,8 @@ jobs: REALM_API_PRIVATE_KEY: ${{ secrets.REALM_API_PRIVATE_KEY }} ROLLBAR_POST_SERVER_ITEM_ACCESS_TOKEN: ${{ secrets.GATSBY_ROLLBAR_TOKEN }} API_MONGODB_CONNECTION_STRING: ${{ secrets.API_MONGODB_CONNECTION_STRING }} + NEXTAUTH_URL: http://localhost:8000 + NEXTAUTH_SECRET: 678x1irXYWeiOqTwCv1awvkAUbO9eHa5xzQEYhxhMms= # only used in local tests - name: Cache build uses: actions/cache/save@v4 diff --git a/.github/workflows/test-playwright-full.yml b/.github/workflows/test-playwright-full.yml index 559ab59ca7..36334acdbe 100644 --- a/.github/workflows/test-playwright-full.yml +++ b/.github/workflows/test-playwright-full.yml @@ -84,11 +84,9 @@ jobs: GATSBY_ALGOLIA_SEARCH_KEY: ${{ vars.GATSBY_ALGOLIA_SEARCH_KEY }} GATSBY_AVAILABLE_LANGUAGES: ${{ vars.GATSBY_AVAILABLE_LANGUAGES }} GATSBY_REALM_APP_ID: ${{ vars.GATSBY_REALM_APP_ID }} - GOOGLE_TRANSLATE_API_KEY: ${{ secrets.GOOGLE_TRANSLATE_API_KEY }} MONGODB_CONNECTION_STRING: mongodb://127.0.0.1:4110/ MONGODB_TRANSLATIONS_CONNECTION_STRING: mongodb://127.0.0.1:4110/ MONGODB_MIGRATIONS_CONNECTION_STRING: mongodb://127.0.0.1:4110/ - TRANSLATE_SUBMISSION_DATE_START: ${{ vars.TRANSLATE_SUBMISSION_DATE_START }} GATSBY_REALM_APP_GRAPHQL_URL: ${{ secrets.GATSBY_REALM_APP_GRAPHQL_URL }} GATSBY_PRISMIC_REPO_NAME: ${{ vars.GATSBY_PRISMIC_REPO_NAME }} PRISMIC_ACCESS_TOKEN: ${{ secrets.PRISMIC_ACCESS_TOKEN }} @@ -137,6 +135,8 @@ jobs: NOTIFICATIONS_SENDER_NAME: Test Preview NOTIFICATIONS_SENDER: test@test.com SITE_URL: http://localhost:8000 + NEXTAUTH_URL: http://localhost:8000 + NEXTAUTH_SECRET: 678x1irXYWeiOqTwCv1awvkAUbO9eHa5xzQEYhxhMms= # only used in local tests - name: Upload Playwright traces if: failure() diff --git a/.github/workflows/test-playwright.yml b/.github/workflows/test-playwright.yml index 7a6a6536d5..bf21b53249 100644 --- a/.github/workflows/test-playwright.yml +++ b/.github/workflows/test-playwright.yml @@ -92,6 +92,8 @@ jobs: NOTIFICATIONS_SENDER_NAME: Test Preview NOTIFICATIONS_SENDER: test@test.com SITE_URL: http://localhost:8000 + NEXTAUTH_URL: http://localhost:8000 + NEXTAUTH_SECRET: 678x1irXYWeiOqTwCv1awvkAUbO9eHa5xzQEYhxhMms= # only used in local tests - uses: actions/upload-artifact@v4 if: ${{ !cancelled() }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000000..c8228851ef --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,134 @@ +name: Run tests +on: + workflow_call: + inputs: + environment: + description: The Github environment to load secrets from + type: string + required: true + sha: + description: The commit SHA to run the tests against + type: string + required: true + runner-label: + description: The label of the runner to use + type: string + cache-modifier: + description: A modifier for the cache key used to bypass existing cache + type: string + required: false + default: "" + +jobs: + test: + name: Run Cypress tests + environment: ${{ inputs.environment }} + runs-on: + labels: ${{ inputs.runner-label || 'ubuntu-latest' }} + defaults: + run: + shell: bash + working-directory: site/gatsby-site + strategy: + # when one test fails, DO NOT cancel the other + # containers, because this will kill Cypress processes + # leaving the Dashboard hanging ... + # https://github.com/cypress-io/github-action/issues/48 + fail-fast: false + matrix: + # run 4 copies of the current job in parallel + containers: [1, 2, 3, 4] + # stop the job if it runs over 20 minutes + # to prevent a hanging process from using all your CI minutes + timeout-minutes: 80 + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ inputs.sha }} + + - name: Read node modules from cache + id: cache-nodemodules + uses: actions/cache/restore@v4 + env: + cache-name: cache-install-folder + with: + path: | + site/gatsby-site/node_modules + ~/.cache/Cypress + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}-${{ inputs.cache-modifier }} + + - name: Install NPM dependencies + if: steps.cache-nodemodules.outputs.cache-hit != 'true' + uses: cypress-io/github-action@v6 + with: + working-directory: site/gatsby-site + runTests: false + install-command: npm ci + + - name: Restore build cache + uses: actions/cache/restore@v4 + env: + cache-name: cache-build-folder + with: + path: | + site/gatsby-site/public + site/gatsby-site/netlify/functions + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ inputs.sha }}-${{ inputs.cache-modifier }} + + - name: Extract branch name + shell: bash + run: echo "branch=$(echo ${GITHUB_REF#refs/heads/})" >> $GITHUB_ENV + id: extract_branch + + - name: Cypress run + uses: cypress-io/github-action@v6 + with: + working-directory: site/gatsby-site + install: false + config-file: cypress.config.js + record: true + parallel: true + group: "Cypress e2e tests" + tag: ${{ steps.extract_branch.outputs.branch }} + start: npx -y pm2 start npm --name "web-server" -- run serve && npx pm2 logs "web-server" + wait-on: http://127.0.0.1:8000 + wait-on-timeout: 60 + env: + # Recommended: pass the GitHub token lets this action correctly + # determine the unique run id necessary to re-run the checks + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CYPRESS_PROJECT_ID: ${{ vars.CYPRESS_PROJECT_ID }} + CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} + E2E_ADMIN_USERNAME: ${{ secrets.E2E_ADMIN_USERNAME }} + E2E_ADMIN_PASSWORD: ${{ secrets.E2E_ADMIN_PASSWORD }} + ALGOLIA_ADMIN_KEY: ${{ secrets.ALGOLIA_ADMIN_KEY }} + GATSBY_ALGOLIA_APP_ID: ${{ vars.GATSBY_ALGOLIA_APP_ID }} + GATSBY_ALGOLIA_SEARCH_KEY: ${{ vars.GATSBY_ALGOLIA_SEARCH_KEY }} + GATSBY_AVAILABLE_LANGUAGES: ${{ vars.GATSBY_AVAILABLE_LANGUAGES }} + GATSBY_REALM_APP_ID: ${{ vars.GATSBY_REALM_APP_ID }} + MONGODB_CONNECTION_STRING: ${{ secrets.MONGODB_CONNECTION_STRING }} + MONGODB_REPLICA_SET: ${{ secrets.MONGODB_REPLICA_SET }} + MONGODB_TRANSLATIONS_CONNECTION_STRING: ${{ secrets.MONGODB_TRANSLATIONS_CONNECTION_STRING }} + MONGODB_MIGRATIONS_CONNECTION_STRING: ${{ secrets.MONGODB_MIGRATIONS_CONNECTION_STRING }} + GATSBY_REALM_APP_GRAPHQL_URL: ${{ secrets.GATSBY_REALM_APP_GRAPHQL_URL }} + GATSBY_ROLLBAR_TOKEN: ${{ secrets.GATSBY_ROLLBAR_TOKEN }} + INSTRUMENT: true + # Since this is triggered on a pull request, we set the commit message to the pull request title + COMMIT_INFO_MESSAGE: ${{ github.event.pull_request.title }} + CLOUDFLARE_R2_ACCOUNT_ID: ${{ vars.CLOUDFLARE_R2_ACCOUNT_ID }} + CLOUDFLARE_R2_BUCKET_NAME: ${{ vars.CLOUDFLARE_R2_BUCKET_NAME }} + GATSBY_CLOUDFLARE_R2_PUBLIC_BUCKET_URL: ${{ vars.GATSBY_CLOUDFLARE_R2_PUBLIC_BUCKET_URL }} + CLOUDFLARE_R2_ACCESS_KEY_ID: ${{ secrets.CLOUDFLARE_R2_ACCESS_KEY_ID }} + CLOUDFLARE_R2_SECRET_ACCESS_KEY: ${{ secrets.CLOUDFLARE_R2_SECRET_ACCESS_KEY }} + REALM_GRAPHQL_API_KEY: ${{ secrets.REALM_GRAPHQL_API_KEY }} + REALM_APP_ID: ${{ secrets.REALM_APP_ID }} + REALM_API_APP_ID: ${{ vars.REALM_API_APP_ID }} + REALM_API_GROUP_ID: ${{ vars.REALM_API_GROUP_ID }} + REALM_API_PUBLIC_KEY: ${{ secrets.REALM_API_PUBLIC_KEY }} + REALM_API_PRIVATE_KEY: ${{ secrets.REALM_API_PRIVATE_KEY }} + + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v4 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/translate-production.yml b/.github/workflows/translate-production.yml new file mode 100644 index 0000000000..a5d9cad247 --- /dev/null +++ b/.github/workflows/translate-production.yml @@ -0,0 +1,15 @@ +name: Translate Reports - Production + +on: + schedule: + - cron: "0 5 * * *" # Run every day at 5 AM GMT + + workflow_dispatch: + +jobs: + call-translate: + uses: ./.github/workflows/translate.yml + secrets: inherit + with: + environment: production + diff --git a/.github/workflows/translate-staging.yml b/.github/workflows/translate-staging.yml new file mode 100644 index 0000000000..4b839bc475 --- /dev/null +++ b/.github/workflows/translate-staging.yml @@ -0,0 +1,15 @@ +name: Translate Reports - Staging + +on: + schedule: + - cron: "0 5 * * *" # Run every day at 5 AM GMT + + workflow_dispatch: + +jobs: + call-translate: + uses: ./.github/workflows/translate.yml + secrets: inherit + with: + environment: staging + diff --git a/.github/workflows/translate.yml b/.github/workflows/translate.yml new file mode 100644 index 0000000000..5bfeacd82b --- /dev/null +++ b/.github/workflows/translate.yml @@ -0,0 +1,38 @@ +name: Translate Reports + +on: + workflow_call: + inputs: + environment: + description: The Github environment to load secrets from + type: string + required: true + +jobs: + translate: + environment: ${{ inputs.environment }} + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: lts/* + + - name: Install NPM dependencies + run: npm ci + working-directory: site/gatsby-site + + - name: Run translation script + run: npm run translate-reports + working-directory: site/gatsby-site + env: + MONGODB_TRANSLATIONS_CONNECTION_STRING: ${{ secrets.MONGODB_TRANSLATIONS_CONNECTION_STRING }} + GOOGLE_TRANSLATE_API_KEY: ${{ secrets.GOOGLE_TRANSLATE_API_KEY }} + GATSBY_AVAILABLE_LANGUAGES: ${{ vars.GATSBY_AVAILABLE_LANGUAGES }} + TRANSLATE_SUBMISSION_DATE_START: ${{ vars.TRANSLATE_SUBMISSION_DATE_START }} + TRANSLATE_DRY_RUN: ${{ vars.TRANSLATE_DRY_RUN }} + diff --git a/README.md b/README.md index 16b607b53e..10b06390f9 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,8 @@     + +     diff --git a/site/gatsby-site/blog/join-raic/index.fr.mdx b/site/gatsby-site/blog/join-raic/index.fr.mdx index 7a419b07fc..94466db376 100644 --- a/site/gatsby-site/blog/join-raic/index.fr.mdx +++ b/site/gatsby-site/blog/join-raic/index.fr.mdx @@ -1,5 +1,5 @@ --- -titre: "Rejoignez l'équipe fondatrice de Responsible AI Collaborative" +title: "Rejoignez l'équipe fondatrice de Responsible AI Collaborative" metaTitle: "Rejoignez l'équipe fondatrice de Responsible AI Collaborative" metaDescription: '' date: '2022-03-29' diff --git a/site/gatsby-site/gatsby-node.js b/site/gatsby-site/gatsby-node.js index 99fa3f5c34..7161cdbd68 100644 --- a/site/gatsby-site/gatsby-node.js +++ b/site/gatsby-site/gatsby-node.js @@ -4,8 +4,6 @@ const fs = require('fs'); const { Client: GoogleMapsAPIClient } = require('@googlemaps/google-maps-services-js'); -const { Translate } = require('@google-cloud/translate').v2; - const { startCase, differenceWith } = require('lodash'); const config = require('./config'); @@ -38,8 +36,6 @@ const createMissingTranslationsPage = require('./page-creators/createMissingTran const algoliasearch = require('algoliasearch'); -const Translator = require('./src/utils/Translator'); - const { MongoClient } = require('mongodb'); const { getLanguages } = require('./i18n'); @@ -286,10 +282,11 @@ exports.onPreBootstrap = async ({ reporter }) => { migrationsActivity.end(); } + // Algolia index update process if (process.env.CONTEXT === 'production') { - const translationsActivity = reporter.activityTimer(`Translations`); + const algoliaUpdaterActivity = reporter.activityTimer(`Algolia`); - translationsActivity.start(); + algoliaUpdaterActivity.start(); const configuredLanguages = getLanguages(); @@ -309,30 +306,15 @@ exports.onPreBootstrap = async ({ reporter }) => { if ( config.mongodb.translationsConnectionString && - config.i18n.translateApikey && config.i18n.availableLanguages && config.header.search.algoliaAdminKey && config.header.search.algoliaAppId ) { - if (process.env.TRANSLATE_DRY_RUN !== 'false') { - reporter.warn( - 'Please set `TRANSLATE_DRY_RUN=false` to disable dry running of translation process.' - ); - } - - translationsActivity.setStatus('Translating incident reports...'); - - const translateClient = new Translate({ key: config.i18n.translateApikey }); - const mongoClient = new MongoClient(config.mongodb.translationsConnectionString); const languages = getLanguages(); - const translator = new Translator({ mongoClient, translateClient, languages, reporter }); - - await translator.run(); - - translationsActivity.setStatus('Updating incidents indexes...'); + algoliaUpdaterActivity.setStatus('Updating Algolia incidents indexes...'); try { const algoliaClient = algoliasearch( @@ -352,12 +334,12 @@ exports.onPreBootstrap = async ({ reporter }) => { reporter.panicOnBuild('Error updating Algolia index:', e); } } else { - throw `Missing environment variable, can't run translation process.`; + throw `Missing environment variable, can't run Algolia update process.`; } - translationsActivity.end(); + algoliaUpdaterActivity.end(); } else { - reporter.warn('Netlify CONTEXT is not production, skipping translations.'); + reporter.warn('Netlify CONTEXT is not production, skipping Algolia index update process.'); } }; diff --git a/site/gatsby-site/migrations/2024.11.13T19.53.42.delete-all-translation-placeholders.js b/site/gatsby-site/migrations/2024.11.13T19.53.42.delete-all-translation-placeholders.js new file mode 100644 index 0000000000..3c31d0ba36 --- /dev/null +++ b/site/gatsby-site/migrations/2024.11.13T19.53.42.delete-all-translation-placeholders.js @@ -0,0 +1,24 @@ +const languages = require('../i18n/config.json'); + +/** + * + * @param {{context: {client: import('mongodb').MongoClient}}} context + */ + +exports.up = async ({ context: { client } }) => { + const db = client.db('translations'); + + for (const language of languages) { + const name = `reports_${language.code}`; + + console.log(`Deleting dummy translations from ${name}`); + + const translations = db.collection(name); + + const result = await translations.deleteMany({ + text: /^translated-/, + }); + + console.log(`Deleted ${result.deletedCount} dummy translations for ${language.code}`); + } +}; diff --git a/site/gatsby-site/package.json b/site/gatsby-site/package.json index 63f1a9b2ab..80e8cf9db6 100644 --- a/site/gatsby-site/package.json +++ b/site/gatsby-site/package.json @@ -138,9 +138,10 @@ "codegen": "graphql-codegen --config codegen.ts", "start:api": "node --env-file=.env --watch -r ts-node/register server/index.ts", "start:memory-mongo": "node --env-file=.env -r ts-node/register playwright/memory-mongo.ts", + "start:memory-mongo:ci": "node -r ts-node/register playwright/memory-mongo.ts", "process-notifications": "node --env-file=.env -r ts-node/register src/scripts/process-notifications.ts", "process-notifications:ci": "node -r ts-node/register src/scripts/process-notifications.ts", - "start:memory-mongo:ci": "node -r ts-node/register playwright/memory-mongo.ts" + "translate-reports": "node src/scripts/translateReports.js" }, "devDependencies": { "@babel/plugin-proposal-export-default-from": "^7.23.3", diff --git a/site/gatsby-site/playwright/e2e-full/subscription.spec.ts b/site/gatsby-site/playwright/e2e-full/subscription.spec.ts index bca263bf6f..fa3b91eb15 100644 --- a/site/gatsby-site/playwright/e2e-full/subscription.spec.ts +++ b/site/gatsby-site/playwright/e2e-full/subscription.spec.ts @@ -13,7 +13,7 @@ test.describe('Subscriptions', () => { await init(); - const [userId] = await login(process.env.E2E_ADMIN_USERNAME, process.env.E2E_ADMIN_PASSWORD, { customData: { roles: ['admin'], first_name: 'John', last_name: 'Doe' } }); + const [userId] = await login(process.env.E2E_ADMIN_USERNAME, process.env.E2E_ADMIN_PASSWORD, { customData: { roles: ['subscriber'], first_name: 'John', last_name: 'Doe' } }); const subscriptions: DBSubscription[] = [ { @@ -36,7 +36,7 @@ test.describe('Subscriptions', () => { test("Incident Updates: Should display a information message if the user doesn't have subscriptions", async ({ page, login }) => { await init(); - await login(process.env.E2E_ADMIN_USERNAME, process.env.E2E_ADMIN_PASSWORD, { customData: { roles: ['admin'], first_name: 'John', last_name: 'Doe' } }); + await login(process.env.E2E_ADMIN_USERNAME, process.env.E2E_ADMIN_PASSWORD, { customData: { roles: ['subscriber'], first_name: 'John', last_name: 'Doe' } }); await page.goto(url); @@ -47,7 +47,7 @@ test.describe('Subscriptions', () => { await init(); - const [userId, accessToken] = await login(process.env.E2E_ADMIN_USERNAME, process.env.E2E_ADMIN_PASSWORD, { customData: { roles: ['admin'], first_name: 'John', last_name: 'Doe' } }); + const [userId, accessToken] = await login(process.env.E2E_ADMIN_USERNAME, process.env.E2E_ADMIN_PASSWORD, { customData: { roles: ['subscriber'], first_name: 'John', last_name: 'Doe' } }); const subscriptions: DBSubscription[] = [ { @@ -96,7 +96,7 @@ test.describe('Subscriptions', () => { test('New Incidents: Should display the switch toggle off if user does not have a subscription', async ({ page, login }) => { - await login(process.env.E2E_ADMIN_USERNAME, process.env.E2E_ADMIN_PASSWORD, { customData: { roles: ['admin'], first_name: 'John', last_name: 'Doe' } }); + await login(process.env.E2E_ADMIN_USERNAME, process.env.E2E_ADMIN_PASSWORD, { customData: { roles: ['subscriber'], first_name: 'John', last_name: 'Doe' } }); await page.goto(url); @@ -109,7 +109,7 @@ test.describe('Subscriptions', () => { await init(); - const [userId] = await login(process.env.E2E_ADMIN_USERNAME, process.env.E2E_ADMIN_PASSWORD, { customData: { roles: ['admin'], first_name: 'John', last_name: 'Doe' } }); + const [userId] = await login(process.env.E2E_ADMIN_USERNAME, process.env.E2E_ADMIN_PASSWORD, { customData: { roles: ['subscriber'], first_name: 'John', last_name: 'Doe' } }); const subscriptions: DBSubscription[] = [ { @@ -152,7 +152,7 @@ test.describe('Subscriptions', () => { await init(); - const [userId] = await login(process.env.E2E_ADMIN_USERNAME, process.env.E2E_ADMIN_PASSWORD, { customData: { roles: ['admin'], first_name: 'John', last_name: 'Doe' } }); + const [userId] = await login(process.env.E2E_ADMIN_USERNAME, process.env.E2E_ADMIN_PASSWORD, { customData: { roles: ['subscriber'], first_name: 'John', last_name: 'Doe' } }); const subscriptions: DBSubscription[] = [ { @@ -182,7 +182,7 @@ test.describe('Subscriptions', () => { await init(); - const [userId] = await login(process.env.E2E_ADMIN_USERNAME, process.env.E2E_ADMIN_PASSWORD, { customData: { roles: ['admin'], first_name: 'John', last_name: 'Doe' } }); + const [userId] = await login(process.env.E2E_ADMIN_USERNAME, process.env.E2E_ADMIN_PASSWORD, { customData: { roles: ['subscriber'], first_name: 'John', last_name: 'Doe' } }); const subscriptions: DBSubscription[] = [ { @@ -212,7 +212,7 @@ test.describe('Subscriptions', () => { await init(); - await login(process.env.E2E_ADMIN_USERNAME, process.env.E2E_ADMIN_PASSWORD, { customData: { roles: ['admin'], first_name: 'John', last_name: 'Doe' } }); + await login(process.env.E2E_ADMIN_USERNAME, process.env.E2E_ADMIN_PASSWORD, { customData: { roles: ['subscriber'], first_name: 'John', last_name: 'Doe' } }); await page.goto(url); @@ -224,7 +224,7 @@ test.describe('Subscriptions', () => { await init(); - const [userId, accessToken] = await login(process.env.E2E_ADMIN_USERNAME, process.env.E2E_ADMIN_PASSWORD, { customData: { roles: ['admin'], first_name: 'John', last_name: 'Doe' } }); + const [userId, accessToken] = await login(process.env.E2E_ADMIN_USERNAME, process.env.E2E_ADMIN_PASSWORD, { customData: { roles: ['subscriber'], first_name: 'John', last_name: 'Doe' } }); const subscriptions: DBSubscription[] = [ { diff --git a/site/gatsby-site/playwright/e2e/translationBadge.spec.ts b/site/gatsby-site/playwright/e2e-full/translationBadge.spec.ts similarity index 76% rename from site/gatsby-site/playwright/e2e/translationBadge.spec.ts rename to site/gatsby-site/playwright/e2e-full/translationBadge.spec.ts index 5964c8845b..c86902cf79 100644 --- a/site/gatsby-site/playwright/e2e/translationBadge.spec.ts +++ b/site/gatsby-site/playwright/e2e-full/translationBadge.spec.ts @@ -20,9 +20,14 @@ test.describe('Translation Badges', () => { await expect(page.locator('[data-cy="5d34b8c29ced494f010ed45c"]').locator('[data-cy="translation-badge"]').getByText('Traducido por IA')).toBeVisible(); }); - test('Should be visible on an incident card on the citation page', async ({ page, skipOnEmptyEnvironment }) => { - await page.goto('/es/cite/1#r1'); - await expect(page.locator('#r1').locator('[data-cy="translation-badge"]').getByText('Traducido por IA')).toBeVisible(); + test('Should be visible on an report card on the citation page if it was translated', async ({ page, skipOnEmptyEnvironment }) => { + await page.goto('/es/cite/3#r3'); + await expect(page.locator('#r3').locator('[data-cy="translation-badge"]').getByText('Traducido por IA')).toBeVisible(); + }); + + test('Should not be visible on an report card on the citation page if it was not translated', async ({ page, skipOnEmptyEnvironment }) => { + await page.goto('/es/cite/3#r4'); + await expect(page.locator('#r4').locator('[data-cy="translation-badge"]').getByText('Traducido por IA')).not.toBeVisible(); }); test('Should be visible on documentation pages', async ({ page }) => { diff --git a/site/gatsby-site/playwright/e2e/blog.spec.ts b/site/gatsby-site/playwright/e2e/blog.spec.ts index 35c79b1473..42d384a7d1 100644 --- a/site/gatsby-site/playwright/e2e/blog.spec.ts +++ b/site/gatsby-site/playwright/e2e/blog.spec.ts @@ -3,6 +3,66 @@ import { expect } from '@playwright/test'; test.describe('Blog', () => { + test('Should load mdx blog post', async ({ page, skipOnEmptyEnvironment }) => { + await page.setViewportSize({ width: 1280, height: 1000 }); + await page.goto('/blog/the-first-taxonomy-of-ai-incidents'); + + await expect(page.locator('.titleWrapper h1')).toHaveText('The First Taxonomy of AI Incidents'); + + const div = await page.locator("[data-testid='blog-content']"); + const textContent = await div.textContent(); + expect(textContent).toContain('In November the Partnership on AI AI Incident Database (AIID) publicly invited users to instantly search through thousands of pages of text to better understand the limitations of AI products within the real world. Since November, tens of thousands of people from 157 countries have connected to the AIID. Today marks the launch of the next stage of AI Incident Database with its first complete AI incident taxonomy.'); + + }); + + test('Should load mdx blog post in spanish', async ({ page, skipOnEmptyEnvironment }) => { + await page.setViewportSize({ width: 1280, height: 1000 }); + await page.goto('/es/blog/representation-and-imagination'); + + await expect(page.locator('.titleWrapper h1')).toHaveText('Representación e imaginación para prevenir los daños de la IA'); + + const div = await page.locator("[data-testid='blog-content']"); + const textContent = await div.textContent(); + expect(textContent).toContain('La base de datos de incidentes de IA se lanzó públicamente en noviembre de 2020 por Partnership on AI como un panel de control de los daños de IA realizados en el mundo real.'); + + }); + + test('Should load mdx blog post in french', async ({ page, skipOnEmptyEnvironment }) => { + await page.setViewportSize({ width: 1280, height: 1000 }); + await page.goto('/fr/blog/join-raic'); + + await expect(page.locator('.titleWrapper h1')).toHaveText("Rejoignez l'équipe fondatrice de Responsible AI Collaborative"); + + const div = await page.locator("[data-testid='blog-content']"); + const textContent = await div.textContent(); + expect(textContent).toContain("La base de données d'incidents d'IA lancée publiquement en novembre 2020 en tant que tableau de bord des dommages causés par l'IA dans le monde réel."); + + }); + + test('Should load prismic blog post', async ({ page, skipOnEmptyEnvironment }) => { + await page.setViewportSize({ width: 1280, height: 1000 }); + await page.goto('/blog/incident-report-2024-january'); + + await expect(page.locator('h1')).toHaveText('AI Incident Roundup – January ‘24'); + + const div = await page.locator("[data-testid='blog-content']"); + const textContent = await div.textContent(); + expect(textContent).toContain('Read our month-in-review newsletter recapping new incidents in the AI Incident Database and looking at the trends.'); + + }); + + test('Should load prismic blog post in spanish', async ({ page, skipOnEmptyEnvironment }) => { + await page.setViewportSize({ width: 1280, height: 1000 }); + await page.goto('/es/blog/incident-report-2024-january'); + + await expect(page.locator('h1')).toHaveText('Resumen de incidentes de IA: 24 de enero'); + + const div = await page.locator("[data-testid='blog-content']"); + const textContent = await div.textContent(); + expect(textContent).toContain('Lea nuestro boletín informativo mensual que resume los nuevos incidentes en la base de datos de incidentes de IA y analiza las tendencias.'); + + }); + test('Should include outline in blog post', async ({ page, skipOnEmptyEnvironment }) => { await page.setViewportSize({ width: 1280, height: 1000 }); await page.goto('/blog/the-first-taxonomy-of-ai-incidents'); diff --git a/site/gatsby-site/playwright/e2e/doc.spec.ts b/site/gatsby-site/playwright/e2e/doc.spec.ts new file mode 100644 index 0000000000..96fd4de374 --- /dev/null +++ b/site/gatsby-site/playwright/e2e/doc.spec.ts @@ -0,0 +1,129 @@ +import { test } from '../utils'; +import { expect } from '@playwright/test'; + +test.describe('Docs', () => { + + test('Should load mdx document in English', async ({ page, skipOnEmptyEnvironment }) => { + await page.setViewportSize({ width: 1280, height: 1000 }); + await page.goto('/research/4-related-work'); + + await expect(page.locator('.titleWrapper h1')).toHaveText('Related Work'); + + const div = await page.locator("[data-testid='doc-content']"); + const textContent = await div.textContent(); + expect(textContent).toContain('While formal AI incident research is relatively new, a number of people have been collecting what could be considered incidents. These include,'); + + }); + + test('Should load mdx doc in spanish', async ({ page, skipOnEmptyEnvironment }) => { + await page.setViewportSize({ width: 1280, height: 1000 }); + await page.goto('/es/research/4-related-work'); // This doc hasn't been translated yet to Spanish yet, so it should fallback to English + + await expect(page.locator('.titleWrapper h1')).toHaveText('Related Work'); + + const div = await page.locator("[data-testid='doc-content']"); + const textContent = await div.textContent(); + expect(textContent).toContain('While formal AI incident research is relatively new, a number of people have been collecting what could be considered incidents. These include,'); + + }); + + test('Should load mdx doc in french', async ({ page, skipOnEmptyEnvironment }) => { + + await page.setViewportSize({ width: 1280, height: 1000 }); + await page.goto('/fr/research/4-related-work'); // This doc hasn't been translated to French yet, so it will default to English + + await expect(page.locator('.titleWrapper h1')).toHaveText('Related Work'); + + const div = await page.locator("[data-testid='doc-content']"); + const textContent = await div.textContent(); + expect(textContent).toContain('While formal AI incident research is relatively new, a number of people have been collecting what could be considered incidents. These include,'); + + }); + + test('Should load mdx doc in japanese', async ({ page, skipOnEmptyEnvironment }) => { + + await page.setViewportSize({ width: 1280, height: 1000 }); + await page.goto('/ja/research/4-related-work'); + + await expect(page.locator('.titleWrapper h1')).toHaveText('関連する研究'); + + const div = await page.locator("[data-testid='doc-content']"); + const textContent = await div.textContent(); + expect(textContent).toContain('公式なAIインシデント研究は比較的新しいものですが、何人かの人々がインシデントと見なされる可能性のある事例を収集しています。これには'); + + }); + + test('Should load prismic doc post', async ({ page, skipOnEmptyEnvironment }) => { + await page.setViewportSize({ width: 1280, height: 1000 }); + await page.goto('/about'); + + await expect(page.locator('h1')).toHaveText('About'); + + const div = await page.locator("[data-testid='markdown-content']"); + const textContent = await div.textContent(); + expect(textContent).toContain('Intelligent systems are currently prone to unforeseen and often dangerous failures when they are deployed to the real world.'); + + }); + + test('Should load prismic doc post in spanish', async ({ page, skipOnEmptyEnvironment }) => { + await page.setViewportSize({ width: 1280, height: 1000 }); + await page.goto('/es/about'); + + await expect(page.locator('h1')).toHaveText('Acerca de'); + + const div = await page.locator("[data-testid='markdown-content']"); + const textContent = await div.textContent(); + expect(textContent).toContain('Actualmente, los sistemas inteligentes son propensos a sufrir fallos imprevistos y, a menudo, peligrosos cuando se implementan en el mundo real.'); + + }); + + test('Should include outline in About page', async ({ page, skipOnEmptyEnvironment }) => { + await page.setViewportSize({ width: 1280, height: 1000 }); + await page.goto('/about'); + + const outlineItems = page.locator('[data-cy="outline"] > li'); + await expect(outlineItems).toHaveCount(7); + + await expect(page.locator('[data-cy="outline"]:has-text("Why \\"AI Incidents\\"?")')).toBeVisible(); + await expect(page.locator('[data-cy="outline"]:has-text("What is an Incident?")')).toBeVisible(); + await expect(page.locator('[data-cy="outline"]:has-text("Current and Future Users")')).toBeVisible(); + await expect(page.locator('[data-cy="outline"]:has-text("When Should You Report an Incident?")')).toBeVisible(); + await expect(page.locator('[data-cy="outline"]:has-text("Board of Directors")')).toBeVisible(); + await expect(page.locator('[data-cy="outline"]:has-text("Collaborators")')).toBeVisible(); + + await page.setViewportSize({ width: 800, height: 1000 }); + + + await expect(page.locator('[data-cy="outline"]:has-text("Why \\"AI Incidents\\"?")')).not.toBeVisible(); + await expect(page.locator('[data-cy="outline"]:has-text("What is an Incident?")')).not.toBeVisible(); + await expect(page.locator('[data-cy="outline"]:has-text("Current and Future Users")')).not.toBeVisible(); + await expect(page.locator('[data-cy="outline"]:has-text("When Should You Report an Incident?")')).not.toBeVisible(); + await expect(page.locator('[data-cy="outline"]:has-text("Board of Directors")')).not.toBeVisible(); + await expect(page.locator('[data-cy="outline"]:has-text("Collaborators")')).not.toBeVisible(); + }); + + test('Should include outline in Spanish About page', async ({ page, skipOnEmptyEnvironment }) => { + await page.setViewportSize({ width: 1280, height: 1000 }); + await page.goto('/es/about'); + + const outlineItems = page.locator('[data-cy="outline"] > li'); + await expect(outlineItems).toHaveCount(7); + + await expect(page.locator('[data-cy="outline"]:has-text("¿Por qué \\"incidentes de IA\\"?")')).toBeVisible(); + await expect(page.locator('[data-cy="outline"]:has-text("¿Qué es un incidente?")')).toBeVisible(); + await expect(page.locator('[data-cy="outline"]:has-text("Usuarios actuales y futuros")')).toBeVisible(); + await expect(page.locator('[data-cy="outline"]:has-text("¿Cuándo debería informar un incidente?")')).toBeVisible(); + await expect(page.locator('[data-cy="outline"]:has-text("Junta Directiva")')).toBeVisible(); + await expect(page.locator('[data-cy="outline"]:has-text("Colaboradores")')).toBeVisible(); + + await page.setViewportSize({ width: 800, height: 1000 }); + + + await expect(page.locator('[data-cy="outline"]:has-text("¿Por qué \\"incidentes de IA\\"?")')).not.toBeVisible(); + await expect(page.locator('[data-cy="outline"]:has-text("¿Qué es un incidente?")')).not.toBeVisible(); + await expect(page.locator('[data-cy="outline"]:has-text("Usuarios actuales y futuros")')).not.toBeVisible(); + await expect(page.locator('[data-cy="outline"]:has-text("¿Cuándo debería informar un incidente?")')).not.toBeVisible(); + await expect(page.locator('[data-cy="outline"]:has-text("Junta Directiva")')).not.toBeVisible(); + await expect(page.locator('[data-cy="outline"]:has-text("Colaboradores")')).not.toBeVisible(); + }); +}); \ No newline at end of file diff --git a/site/gatsby-site/playwright/e2e/unit/translator.spec.ts b/site/gatsby-site/playwright/e2e/unit/translator.spec.ts index e998bbeb03..35b64d1725 100644 --- a/site/gatsby-site/playwright/e2e/unit/translator.spec.ts +++ b/site/gatsby-site/playwright/e2e/unit/translator.spec.ts @@ -1,12 +1,17 @@ import { ObjectId } from 'bson'; import { test } from '../../utils'; -import Translator from '../../../src/utils/Translator'; +import Translator from '../../../src/scripts/Translator'; import sinon from 'sinon'; const reports = [ { _id: new ObjectId('60dd465f80935bc89e6f9b01'), - authors: ['Alistair Barr'], + report_number: 1, + title: 'Report 1 title', + text: 'Report 1 **text**', + plain_text: 'Report 1 text', + language: 'en', + authors: ['Jhon Doe'], date_downloaded: '2019-04-13', date_modified: '2020-06-14', date_published: '2015-05-19', @@ -17,61 +22,65 @@ const reports = [ epoch_date_published: 1431993600, epoch_date_submitted: 1559347200, image_url: 'http://url.com', - language: 'en', - report_number: 1, source_domain: 'blogs.wsj.com', - submitters: ['Roman Yampolskiy'], + submitters: ['Jason Smith'], tags: [], - text: 'Report 1 **text**', - plain_text: 'Report 1 text', - title: 'Report 1 title', url: 'https://url.com/stuff', }, { _id: new ObjectId('60dd465f80935bc89e6f9b02'), - authors: ['Alistair Barr'], + report_number: 2, + title: 'Título del reporte 2', + text: 'Reporte 2 **texto**', + plain_text: 'Reporte 2 texto', + language: 'es', + authors: ['Juan Perez'], date_downloaded: '2019-04-13', date_modified: '2020-06-14', date_published: '2015-05-19', - date_submitted: '2019-06-01', - description: 'Description of report 2', + date_submitted: '2020-06-01', + description: 'Descripción del reporte 2', epoch_date_downloaded: 1555113600, epoch_date_modified: 1592092800, epoch_date_published: 1431993600, epoch_date_submitted: 1559347200, image_url: 'http://url.com', + source_domain: 'blogs.wsj.com', + submitters: ['Ramon Gomez'], + tags: [], + url: 'https://url.com/stuff', + }, + { + _id: new ObjectId('60dd465f80935bc89e6f9b03'), + report_number: 3, + title: 'Título del reporte 3', + text: 'Reporte 3 **texto**', + plain_text: 'Reporte 3 texto', language: 'es', - report_number: 2, + authors: ['Juan Perez'], + date_downloaded: '2019-04-13', + date_modified: '2020-06-14', + date_published: '2015-05-19', + date_submitted: '2021-06-01', + description: 'Descripción del reporte 3', + epoch_date_downloaded: 1555113600, + epoch_date_modified: 1592092800, + epoch_date_published: 1431993600, + epoch_date_submitted: 1559347200, + image_url: 'http://url.com', source_domain: 'blogs.wsj.com', - submitters: ['Roman Yampolskiy'], + submitters: ['Ramon Gomez'], tags: [], - text: 'Report 2 **text**', - plain_text: 'Report 2 text', - title: 'Report 2 title', url: 'https://url.com/stuff', }, ]; test('Translations - Should translate languages only if report language differs from target language', async ({ page }) => { - const translatedReportsEN = [ - { - _id: '61d5ad9f102e6e30fca90ddf', - text: 'translated-en-text report 1', - title: 'translated-en-title report 1', - report_number: 1, - }, - ]; + const translatedReportsEN = []; - const translatedReportsES = [ - { - _id: '61d5ad9f102e6e30fca90ddf', - text: 'translated-es-text report 2', - title: 'translated-es-title report 2', - report_number: 2, - }, - ]; + const translatedReportsES = []; - const reporter = { log: sinon.stub() }; + const reporter = { log: sinon.stub(), error: sinon.stub(), warn: sinon.stub() }; const reportsCollection = { find: sinon.stub().returns({ @@ -121,13 +130,23 @@ test('Translations - Should translate languages only if report language differs await translator.run(); sinon.assert.calledOnce(mongoClient.connect); + sinon.assert.calledOnce(reportsCollection.find); + sinon.assert.calledThrice(translateClient.translate); sinon.assert.calledOnce(reportsENCollection.insertMany); - sinon.assert.calledWith(reportsENCollection.insertMany, [{ - report_number: 2, - text: 'test-en-Report 2 **text**', - title: 'test-en-Report 2 title', - plain_text: 'test-en-Report 2 text\n', - }]); + sinon.assert.calledWith(reportsENCollection.insertMany, [ + { + report_number: 2, + text: 'test-en-Reporte 2 **texto**', + title: 'test-en-Título del reporte 2', + plain_text: 'test-en-Reporte 2 texto\n', + }, + { + report_number: 3, + text: 'test-en-Reporte 3 **texto**', + title: 'test-en-Título del reporte 3', + plain_text: 'test-en-Reporte 3 texto\n' + } + ]); sinon.assert.calledOnce(reportsESCollection.insertMany); sinon.assert.calledWith(reportsESCollection.insertMany, [{ report_number: 1, @@ -138,26 +157,191 @@ test('Translations - Should translate languages only if report language differs sinon.assert.calledOnce(mongoClient.close); }); -test("Translations - Shouldn't call Google's translate api if dryRun is true", async ({ page }) => { - const translatedReportsEN = [ +test("Translations - Shouldn't call Google's translate api and use translation placeholders if dryRun is true", async ({ page }) => { + const translatedReportsEN = []; + + const translatedReportsES = []; + + const reporter = { log: sinon.stub(), error: sinon.stub(), warn: sinon.stub() }; + + const reportsCollection = { + find: sinon.stub().returns({ + toArray: sinon.stub().resolves(reports), + }), + }; + + const reportsENCollection = { + find: sinon.stub().returns({ + toArray: sinon.stub().resolves(translatedReportsEN), + }), + insertMany: sinon.stub().resolves({ insertedCount: 1 }), + }; + + const reportsESCollection = { + find: sinon.stub().returns({ + toArray: sinon.stub().resolves(translatedReportsES), + }), + insertMany: sinon.stub().resolves({ insertedCount: 1 }), + }; + + const mongoClient = { + connect: sinon.stub().resolves(), + close: sinon.stub().resolves(), + db: sinon.stub().returns({ + collection: (name: string) => { + if (name === 'reports') return reportsCollection; + if (name === 'reports_en') return reportsENCollection; + if (name === 'reports_es') return reportsESCollection; + return null; + }, + }), + }; + + const translateClient = { + translate: sinon.stub().callsFake((payload, { to }) => [payload.map((p: any) => `translated-${to}-${p}`)]), + }; + + const translator = new Translator({ + mongoClient, + translateClient, + languages: [{ code: 'es' }, { code: 'en' }], + reporter, + dryRun: true, + }); + + await translator.run(); + + sinon.assert.calledOnce(mongoClient.connect); + sinon.assert.calledOnce(reportsCollection.find); + sinon.assert.notCalled(translateClient.translate); + sinon.assert.calledOnce(reportsENCollection.insertMany); + sinon.assert.calledWith(reportsENCollection.insertMany, [ + { + report_number: 2, + text: 'translated-en-Reporte 2 **texto**', + title: 'translated-en-Título del reporte 2', + plain_text: 'translated-en-Reporte 2 texto\n', + }, + { + report_number: 3, + text: 'translated-en-Reporte 3 **texto**', + title: 'translated-en-Título del reporte 3', + plain_text: 'translated-en-Reporte 3 texto\n' + } + ]); + sinon.assert.calledOnce(reportsESCollection.insertMany); + sinon.assert.calledWith(reportsESCollection.insertMany, [{ + report_number: 1, + text: 'translated-es-Report 1 **text**', + title: 'translated-es-Report 1 title', + plain_text: 'translated-es-Report 1 text\n', + }]); + sinon.assert.calledOnce(mongoClient.close); +}); + +test('Translations - Should translate reports with submission date greater than an specific report submission date', async ({ page }) => { + const submissionDateStart = '2021-01-01'; + + const translatedReportsES = []; + + const translatedReportsEN = []; + + const reporter = { log: sinon.stub(), error: sinon.stub(), warn: sinon.stub() }; + + const reportsCollection = { + find: sinon.stub().returns({ + toArray: sinon.stub().resolves(reports.filter(r => r.date_submitted > submissionDateStart)), + }), + }; + + const reportsENCollection = { + find: sinon.stub().returns({ + toArray: sinon.stub().resolves(translatedReportsEN), + }), + insertMany: sinon.stub().resolves({ insertedCount: 1 }), + }; + + const reportsESCollection = { + find: sinon.stub().returns({ + toArray: sinon.stub().resolves(translatedReportsES), + }), + insertMany: sinon.stub().resolves({ insertedCount: 1 }), + }; + + const mongoClient = { + connect: sinon.stub().resolves(), + close: sinon.stub().resolves(), + db: sinon.stub().returns({ + collection: (name: string) => { + if (name === 'reports') return reportsCollection; + if (name === 'reports_en') return reportsENCollection; + if (name === 'reports_es') return reportsESCollection; + return null; + }, + }), + }; + + const translateClient = { + translate: sinon.stub().callsFake((payload, { to }) => [payload.map((p: any) => `test-${to}-${p}`)]), + }; + + const translator = new Translator({ + mongoClient, + translateClient, + languages: [{ code: 'es' }, { code: 'en' }], + reporter, + dryRun: false, + submissionDateStart: submissionDateStart, + }); + + await translator.run(); + + sinon.assert.calledOnce(mongoClient.connect); + sinon.assert.calledOnce(reportsCollection.find); + sinon.assert.calledWith(reportsCollection.find, { date_submitted: { $gte: new Date(submissionDateStart) } }); + sinon.assert.calledOnce(translateClient.translate); + sinon.assert.calledOnce(reportsENCollection.insertMany); + sinon.assert.calledWith(reportsENCollection.insertMany, [ + { + report_number: 3, + text: 'test-en-Reporte 3 **texto**', + title: 'test-en-Título del reporte 3', + plain_text: 'test-en-Reporte 3 texto\n' + } + ]); + sinon.assert.notCalled(reportsESCollection.insertMany); + sinon.assert.calledOnce(mongoClient.close); +}); + +test('Translations - Should not translate if the report was already translated', async ({ page }) => { + const translatedReportsES = [ { _id: '61d5ad9f102e6e30fca90ddf', - text: 'translated-en-text report 1', - title: 'translated-en-title report 1', report_number: 1, + title: 'Título del reporte 1', + text: 'Reporte 1 **texto**', + plain_text: 'Reporte 1 texto', }, ]; - const translatedReportsES = [ + const translatedReportsEN = [ { - _id: '61d5ad9f102e6e30fca90ddf', - text: 'translated-es-text report 2', - title: 'translated-es-title report 2', + _id: '61d5ad9f102e6e30fca90dde', report_number: 2, + title: 'Report 2 title', + text: 'Report 2 **text**', + plain_text: 'Report 2 text', + }, + { + _id: '61d5ad9f102e6e30fca90ddf', + report_number: 3, + title: 'Report 3 title', + text: 'Report 3 **text**', + plain_text: 'Report 3 text', }, ]; - const reporter = { log: sinon.stub() }; + const reporter = { log: sinon.stub(), error: sinon.stub(), warn: sinon.stub() }; const reportsCollection = { find: sinon.stub().returns({ @@ -193,7 +377,7 @@ test("Translations - Shouldn't call Google's translate api if dryRun is true", a }; const translateClient = { - translate: sinon.stub().callsFake((payload, { to }) => [payload.map((p: any) => `translated-${to}-${p}`)]), + translate: sinon.stub().callsFake((payload, { to }) => [payload.map((p: any) => `test-${to}-${p}`)]), }; const translator = new Translator({ @@ -201,10 +385,15 @@ test("Translations - Shouldn't call Google's translate api if dryRun is true", a translateClient, languages: [{ code: 'es' }, { code: 'en' }], reporter, - dryRun: true, + dryRun: false, }); await translator.run(); + sinon.assert.calledOnce(mongoClient.connect); + sinon.assert.calledOnce(reportsCollection.find); sinon.assert.notCalled(translateClient.translate); + sinon.assert.notCalled(reportsENCollection.insertMany); + sinon.assert.notCalled(reportsESCollection.insertMany); + sinon.assert.calledOnce(mongoClient.close); }); diff --git a/site/gatsby-site/plugins/gatsby-theme-i18n/gatsby-node.js b/site/gatsby-site/plugins/gatsby-theme-i18n/gatsby-node.js index 2a3cf7eec6..bfc953315c 100644 --- a/site/gatsby-site/plugins/gatsby-theme-i18n/gatsby-node.js +++ b/site/gatsby-site/plugins/gatsby-theme-i18n/gatsby-node.js @@ -220,50 +220,40 @@ exports.onCreatePage = ({ page, actions }, themeOptions) => { const [template, mdxFile] = page.component.split(`?__contentFilePath=`); - // If the mdxFile path has a language, let's strip the language from it. - // ex: index.de.mdx ==> index.mdx + // Process the mdxFile if it exists if (mdxFile) { - let thePath = path.dirname(mdxFile); + const directory = path.dirname(mdxFile); // Get the directory path - let fileName = path.basename(mdxFile); + const fileName = path.basename(mdxFile, `.mdx`); // Extract the file name without `.mdx` - let ext = path.extname(mdxFile); + const ext = path.extname(mdxFile); // Get the file extension - if (ext !== '.mdx') { - throw new Error(`Unexpected file extension in mdx path parsing: ${mdxFile}`); + if (ext !== `.mdx`) { + throw new Error(`Unexpected file extension in MDX path parsing: ${mdxFile}`); } - // Split the filename in three parts split by the dot. We expect two or three components. - let fileNamePieces = fileName.split('.'); + // Extract the locale from the file name if present (e.g., "index.es" → "es") + const nameParts = fileName.split(`.`); - if (fileNamePieces.length == 2) { - // ex: index.mdx - theFilePath = mdxFile; - } else if (fileNamePieces.length == 3) { - // ex: index.es.mdx - // Keep everything except the language code. - theFilePath = `${path.join(thePath, fileNamePieces.at(0))}${ext}`; - } else { - throw new Error(`Unexpected file format in mdx path parsing: ${mdxFile}`); - } - - // If we use a non-default language, and the language file is on the disk, then use it. - if (ext === '.mdx' && locale.code !== defaultLang) { - // ex: /path/up/to/lang/code - let potentialPath = path.join(path.dirname(thePath), fileNamePieces.at(0)); + let baseName = nameParts[0]; // Default to the base name without locale - // ex: /path/up/to/lang/code.es.mdx - let potentialLangfile = `${potentialPath}.${locale.code}${ext}`; + // If it's a non-default language and the file exists, use the localized file + if (locale.code !== defaultLang) { + const localizedFile = path.join(directory, `${baseName}.${locale.code}${ext}`); - if (fs.existsSync(potentialLangfile)) { - theFilePath = potentialLangfile; + if (fs.existsSync(localizedFile)) { + theFilePath = `${template}?__contentFilePath=${localizedFile}`; } else { - // Nothing to render if file doen't exist. - theFilePath = ''; + // If no localized file exists, use the default file + theFilePath = `${template}?__contentFilePath=${path.join( + directory, + `${baseName}${ext}` + )}`; } + } else { + // For the default language, strip the locale from the path if present + theFilePath = `${template}?__contentFilePath=${path.join(directory, `${baseName}${ext}`)}`; } - - theFilePath = `${template}?__contentFilePath=${theFilePath}`; } const newPage = { @@ -293,7 +283,6 @@ exports.onCreatePage = ({ page, actions }, themeOptions) => { // Check if the page is a localized 404 if (newPage.path.match(/^\/[a-z]{2}\/404\/$/)) { - // Match all paths starting with this code (apart from other valid paths) newPage.matchPath = `/${locale.code}/*`; } diff --git a/site/gatsby-site/server/rules.ts b/site/gatsby-site/server/rules.ts index fb3cc448f5..c9337f0cbb 100644 --- a/site/gatsby-site/server/rules.ts +++ b/site/gatsby-site/server/rules.ts @@ -55,7 +55,7 @@ export const isSubscriptionOwner = () => rule()( const collection = context.client.db('customData').collection('subscriptions'); const simpleType = getSimplifiedType(SubscriptionType); - const filter = getMongoDbFilter(simpleType, info.variableValues.filter as GraphQLFilter); + const filter = getMongoDbFilter(simpleType, args.filter as GraphQLFilter); const subscriptions = await collection.find(filter).toArray(); const { user } = context; diff --git a/site/gatsby-site/src/components/blog/PrismicBlogPost.js b/site/gatsby-site/src/components/blog/PrismicBlogPost.js index 1b677fc8b6..ee82d39c44 100644 --- a/site/gatsby-site/src/components/blog/PrismicBlogPost.js +++ b/site/gatsby-site/src/components/blog/PrismicBlogPost.js @@ -74,7 +74,7 @@ const PrismicBlogPost = ({ post, location }) => { -
+
diff --git a/site/gatsby-site/src/components/doc/PrismicDocPost.js b/site/gatsby-site/src/components/doc/PrismicDocPost.js index 37b80546f7..e9ed8501b0 100644 --- a/site/gatsby-site/src/components/doc/PrismicDocPost.js +++ b/site/gatsby-site/src/components/doc/PrismicDocPost.js @@ -64,7 +64,7 @@ const PrismicDocPost = ({ doc, location }) => { {doc.data.content.map((content, index) => ( <> {content.markdown?.richText.length > 0 && ( -
+
{(() => { const rawMarkdown = RichText.asText(content.markdown.richText); @@ -77,7 +77,7 @@ const PrismicDocPost = ({ doc, location }) => {
)} {content.text && ( -
+
)} diff --git a/site/gatsby-site/src/components/reports/ReportCard.js b/site/gatsby-site/src/components/reports/ReportCard.js index 2756576079..ba199089a0 100644 --- a/site/gatsby-site/src/components/reports/ReportCard.js +++ b/site/gatsby-site/src/components/reports/ReportCard.js @@ -173,7 +173,9 @@ const ReportCard = ({ {actions && !readOnly && <>{actions}}
- + {item.isTranslated && ( + + )} {item.tags && item.tags.includes(RESPONSE_TAG) && (
diff --git a/site/gatsby-site/src/utils/Translator.js b/site/gatsby-site/src/scripts/Translator.js similarity index 76% rename from site/gatsby-site/src/utils/Translator.js rename to site/gatsby-site/src/scripts/Translator.js index e3a1ad72d8..1a2ae362b0 100644 --- a/site/gatsby-site/src/utils/Translator.js +++ b/site/gatsby-site/src/scripts/Translator.js @@ -8,7 +8,23 @@ const remarkStrip = require('strip-markdown'); const keys = ['text', 'title']; +/** + * @typedef {Object} Reporter + * @property {function(string):void} log + * @property {function(string):void} error + * @property {function(string):void} warn + */ + class Translator { + /** + * @param {Object} options + * @param {import('mongodb').MongoClient} options.mongoClient + * @param {Object} options.translateClient + * @param {string[]} options.languages + * @param {Reporter} options.reporter + * @param {string} [options.submissionDateStart] + * @param {boolean} [options.dryRun] + */ constructor({ mongoClient, translateClient, @@ -18,10 +34,6 @@ class Translator { dryRun = process.env.TRANSLATE_DRY_RUN !== 'false', }) { this.translateClient = translateClient; - /** - * @type {import('mongodb').MongoClient} - * @public - */ this.mongoClient = mongoClient; this.reporter = reporter; this.languages = languages; @@ -49,9 +61,12 @@ class Translator { }, concurrency); q.error((err, task) => { - this.reporter.log( + this.reporter.error( `Error translating report ${task.entry.report_number}, ${err.code} ${err.message}` ); + throw new Error( + `Translation process failed for report ${task.entry.report_number}. Error: ${err.code} - ${err.message}` + ); }); const alreadyTranslated = await this.getTranslatedReports({ items, language: to }); @@ -72,20 +87,26 @@ class Translator { async getTranslatedReports({ items, language }) { const originalIds = items.map((item) => item.report_number); - const incidents = this.mongoClient.db('translations').collection(`reports_${language}`); + const reportsTranslatedCollection = this.mongoClient + .db('translations') + .collection(`reports_${language}`); const query = { report_number: { $in: originalIds }, $and: [...keys, 'plain_text'].map((key) => ({ [key]: { $exists: true } })), }; - const translated = await incidents.find(query, { projection: { report_number: 1 } }).toArray(); + const translated = await reportsTranslatedCollection + .find(query, { projection: { report_number: 1 } }) + .toArray(); return translated; } async saveTranslatedReports({ items, language }) { - const incidents = this.mongoClient.db('translations').collection(`reports_${language}`); + const reportsTranslatedCollection = this.mongoClient + .db('translations') + .collection(`reports_${language}`); const translated = []; @@ -97,7 +118,7 @@ class Translator { translated.push({ report_number, text, title, plain_text }); } - return incidents.insertMany(translated); + return reportsTranslatedCollection.insertMany(translated); } async translateReport({ entry, to }) { @@ -125,6 +146,12 @@ class Translator { } async run() { + if (this.dryRun) { + this.reporter.warn( + 'Please set `TRANSLATE_DRY_RUN=false` to disable dry running of translation process.' + ); + } + await this.mongoClient.connect(); let reportsQuery = {}; @@ -135,7 +162,7 @@ class Translator { const errorMessage = `Translation process error: Invalid date format for TRANSLATE_SUBMISSION_DATE_START env variable: [${this.submissionDateStart}]`; this.reporter.error(errorMessage); - throw errorMessage; + throw new Error(errorMessage); } this.reporter.log( @@ -143,7 +170,9 @@ class Translator { ); reportsQuery = { date_submitted: { $gte: new Date(this.submissionDateStart) } }; } else { - this.reporter.log(`Translating all incident reports`); + this.reporter.log( + `Translating all incident reports. (TRANSLATE_SUBMISSION_DATE_START env variable is not defined)` + ); } const reports = await this.mongoClient diff --git a/site/gatsby-site/src/scripts/translate-incident-reports.js b/site/gatsby-site/src/scripts/translate-incident-reports.js deleted file mode 100644 index 8f3cf6e916..0000000000 --- a/site/gatsby-site/src/scripts/translate-incident-reports.js +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Invokes translation process without Gatsby build - * Run with `node ./src/scripts/translate-incident-reports.js` - */ - -require('dotenv').config(); - -const config = require('../../config'); - -const { MongoClient } = require('mongodb'); - -const { Translate } = require('@google-cloud/translate').v2; - -const Translator = require('../utils/Translator'); - -const { getLanguages } = require('../../i18n'); - -const reporter = { log: console.log }; - -(async () => { - console.log('Translating incident reports...'); - - const mongoClient = new MongoClient(config.mongodb.translationsConnectionString); - - const translateClient = new Translate({ key: config.i18n.translateApikey }); - - const translator = new Translator({ - mongoClient, - translateClient, - languages: getLanguages(), - reporter, - }); - - await translator.run(); - - console.log('Done'); - - process.exit(0); -})(); diff --git a/site/gatsby-site/src/scripts/translateReports.js b/site/gatsby-site/src/scripts/translateReports.js new file mode 100644 index 0000000000..9a3448a897 --- /dev/null +++ b/site/gatsby-site/src/scripts/translateReports.js @@ -0,0 +1,60 @@ +require('dotenv').config(); + +const config = require('../../config'); + +const { MongoClient } = require('mongodb'); + +const { Translate } = require('@google-cloud/translate').v2; + +const Translator = require('./Translator'); + +const { getLanguages } = require('../../i18n'); + +const reporter = { log: console.log, error: console.error, warn: console.warn }; + +(async () => { + console.log('Translating incident reports...'); + + let mongoClient; + + try { + // MongoDB client setup + mongoClient = new MongoClient(config.mongodb.translationsConnectionString); + try { + await mongoClient.connect(); + } catch (mongoError) { + throw new Error(`Error connecting to MongoDB: ${mongoError.message}`); + } + + // Google Translate client setup + if (!config.i18n.translateApikey) { + throw new Error('Google Translate API (GOOGLE_TRANSLATE_API_KEY) key is missing.'); + } + const translateClient = new Translate({ key: config.i18n.translateApikey }); + + // Create Translator instance + const translator = new Translator({ + mongoClient, + translateClient, + languages: getLanguages(), + reporter, + }); + + // Run the translation process + await translator.run(); + + console.log('Translation completed successfully.'); + } catch (error) { + console.error('Error during the translation process:', error.message); + process.exit(1); + } finally { + if (mongoClient) { + try { + await mongoClient.close(); + console.log('MongoDB connection closed gracefully.'); + } catch (closeError) { + console.error('Error closing MongoDB connection:', closeError.message); + } + } + } +})(); diff --git a/site/gatsby-site/src/templates/doc.js b/site/gatsby-site/src/templates/doc.js index a11f9edfad..17d38678e7 100644 --- a/site/gatsby-site/src/templates/doc.js +++ b/site/gatsby-site/src/templates/doc.js @@ -44,7 +44,7 @@ export default function Doc(props) {
)}
-
+
{children}
diff --git a/site/gatsby-site/src/templates/post.js b/site/gatsby-site/src/templates/post.js index e3d7d15f03..e052ad44b9 100644 --- a/site/gatsby-site/src/templates/post.js +++ b/site/gatsby-site/src/templates/post.js @@ -72,7 +72,7 @@ export default function Post(props) {
-
+
{children}
diff --git a/site/gatsby-site/src/utils/cite.js b/site/gatsby-site/src/utils/cite.js index a3b3021224..eee6092350 100644 --- a/site/gatsby-site/src/utils/cite.js +++ b/site/gatsby-site/src/utils/cite.js @@ -104,7 +104,14 @@ export const getTranslatedReports = ({ allMongodbAiidprodReports, translations, (t) => t.report_number === r.report_number ); - return translation ? { ...r, text: translation.text, title: translation.title } : { ...r }; + return translation + ? { + ...r, + text: translation.text, + title: translation.title, + isTranslated: true, // Mark badge to display or not + } + : { ...r }; }); };