From c699e7c2f17d95675723ec5752584cd0ec9ef8ef Mon Sep 17 00:00:00 2001 From: Peter Bengtsson Date: Fri, 26 May 2023 08:13:01 -0400 Subject: [PATCH] Test with fixture translations (#37232) --- .github/workflows/test.yml | 3 +- lib/constants.js | 1 + lib/languages.js | 26 +- package.json | 2 +- .../get-started/quickstart/dynamic-title.md | 9 + .../get-started/quickstart/hello-world.md | 16 ++ tests/fixtures/translations/ja-jp/data/ui.yml | 260 ++++++++++++++++++ .../playwright-rendering.spec.ts | 56 ++++ tests/rendering-fixtures/translations.js | 53 ++++ 9 files changed, 420 insertions(+), 6 deletions(-) create mode 100644 tests/fixtures/translations/ja-jp/content/get-started/quickstart/dynamic-title.md create mode 100644 tests/fixtures/translations/ja-jp/content/get-started/quickstart/hello-world.md create mode 100644 tests/fixtures/translations/ja-jp/data/ui.yml create mode 100644 tests/rendering-fixtures/translations.js diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index aa33093c9d03..4d0397b058c4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -173,5 +173,6 @@ jobs: # tests run only in English. The exception is the # `tests/translations/` suite which needs all languages to be set up. ENABLED_LANGUAGES: ${{ matrix.name == 'translations' && 'all' || '' }} - ROOT: ${{ (matrix.name == 'rendering-fixtures' || matrix.name == 'pageinfo') && 'tests/fixtures' || ''}} + ROOT: ${{ (matrix.name == 'rendering-fixtures' || matrix.name == 'pageinfo') && 'tests/fixtures' || '' }} + TRANSLATIONS_FIXTURE_ROOT: ${{ (matrix.name == 'rendering-fixtures' || matrix.name == 'pageinfo') && 'tests/fixtures/translations' || '' }} run: npm test -- ${{ matrix.path }}/ diff --git a/lib/constants.js b/lib/constants.js index 3bd3326198eb..1777f5fb7ed4 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -2,3 +2,4 @@ export const ROOT = process.env.ROOT || '.' export const USER_LANGUAGE_COOKIE_NAME = 'user_language' export const TRANSLATIONS_ROOT = process.env.TRANSLATIONS_ROOT || 'translations' export const MAX_REQUEST_TIMEOUT = parseInt(process.env.REQUEST_TIMEOUT || 10000, 10) +export const TRANSLATIONS_FIXTURE_ROOT = process.env.TRANSLATIONS_FIXTURE_ROOT diff --git a/lib/languages.js b/lib/languages.js index dbf5de521e51..1e6234489a35 100644 --- a/lib/languages.js +++ b/lib/languages.js @@ -1,11 +1,12 @@ // See also languages-schema.js // Nota bene: If you are adding a new language, // change accept-language handling in CDN config as well. +import path from 'path' +import fs from 'fs' import dotenv from 'dotenv' -import { ROOT, TRANSLATIONS_ROOT } from './constants.js' -import path from 'path' +import { ROOT, TRANSLATIONS_ROOT, TRANSLATIONS_FIXTURE_ROOT } from './constants.js' dotenv.config() @@ -22,6 +23,14 @@ const possibleEnvVars = { function getRoot(languageCode) { if (languageCode === 'en') return ROOT + + // This one trumps anything else. This makes it possible, and convenient, + // for running tests that depends on testing translations based on + // fixtures exclusively. + if (TRANSLATIONS_FIXTURE_ROOT) { + return path.join(TRANSLATIONS_FIXTURE_ROOT, languageCode) + } + if (languageCode in possibleEnvVars) { const possibleEnvVar = possibleEnvVars[languageCode] if (possibleEnvVar) { @@ -114,10 +123,19 @@ const languages = { }, } -if (process.env.ENABLED_LANGUAGES) { +if (TRANSLATIONS_FIXTURE_ROOT) { + // Keep all languages that have a directory in the fixture root. + Object.entries(languages).forEach(([code, { dir }]) => { + if (code !== 'en' && !fs.existsSync(dir)) { + delete languages[code] + } + }) +} else if (process.env.ENABLED_LANGUAGES) { if (process.env.ENABLED_LANGUAGES.toLowerCase() !== 'all') { Object.keys(languages).forEach((code) => { - if (!process.env.ENABLED_LANGUAGES.includes(code)) delete languages[code] + if (!process.env.ENABLED_LANGUAGES.includes(code)) { + delete languages[code] + } }) // This makes the translation health report not valid JSON // console.log(`ENABLED_LANGUAGES: ${process.env.ENABLED_LANGUAGES}`) diff --git a/package.json b/package.json index 32a2b7d4259c..0973e5a456bc 100644 --- a/package.json +++ b/package.json @@ -203,7 +203,7 @@ "prestart": "node script/cmp-files.js package-lock.json .installed.package-lock.json || npm install && cp package-lock.json .installed.package-lock.json", "start": "cross-env NODE_ENV=development ENABLED_LANGUAGES=en nodemon server.js", "start-all-languages": "cross-env NODE_ENV=development nodemon server.js", - "start-for-playwright": "cross-env ROOT=tests/fixtures NODE_ENV=test node server.js", + "start-for-playwright": "cross-env ROOT=tests/fixtures TRANSLATIONS_FIXTURE_ROOT=tests/fixtures/translations ENABLED_LANGUAGES=en,ja NODE_ENV=test node server.js", "sync-search": "cross-env NODE_OPTIONS='--max_old_space_size=8192' start-server-and-test sync-search-server 4002 sync-search-indices", "sync-search-ghes-release": "cross-env GHES_RELEASE=1 start-server-and-test sync-search-server 4002 sync-search-indices", "sync-search-indices": "src/search/scripts/sync-search-indices.js", diff --git a/tests/fixtures/translations/ja-jp/content/get-started/quickstart/dynamic-title.md b/tests/fixtures/translations/ja-jp/content/get-started/quickstart/dynamic-title.md new file mode 100644 index 000000000000..d61f90b999ca --- /dev/null +++ b/tests/fixtures/translations/ja-jp/content/get-started/quickstart/dynamic-title.md @@ -0,0 +1,9 @@ +--- +title: '{% ifversion fpt %}こんにちは{% else %}挨拶{% endif %} {% data variables.product.product_name %}' +--- + +## 序章 + +"こんにちは" means "Hello" + +"挨拶" means "Salutation" according to Google Translate diff --git a/tests/fixtures/translations/ja-jp/content/get-started/quickstart/hello-world.md b/tests/fixtures/translations/ja-jp/content/get-started/quickstart/hello-world.md new file mode 100644 index 000000000000..7765709879fb --- /dev/null +++ b/tests/fixtures/translations/ja-jp/content/get-started/quickstart/hello-world.md @@ -0,0 +1,16 @@ +--- +title: 'こんにちは World' +intro: 'この Hello World 演習に従って、{% data variables.product.product_name %} の使用を開始します。' +--- + +## はじめに + +この世界にこんにちはと言ってください + +> This means "Say hello to this world" according to Google Translate. + +## 可変タイトルのページへのリンク + +"[AUTOTITLE](/get-started/quickstart/dynamic-title)" + +"[AUTOTITLE](/get-started/foo/cross-version-linking)" diff --git a/tests/fixtures/translations/ja-jp/data/ui.yml b/tests/fixtures/translations/ja-jp/data/ui.yml new file mode 100644 index 000000000000..3d2501813e85 --- /dev/null +++ b/tests/fixtures/translations/ja-jp/data/ui.yml @@ -0,0 +1,260 @@ +# This file is a snapshot copy from 'docs-internal.ja-jp'. +# It's important to assure that we don't need to have keys for +# everything because the fallback should work. + +meta: + default_description: GitHub の使用開始、トラブルシューティング、最大限に活用する方法について説明します。 新規ユーザー、開発者、管理者、すべての GitHub の製品に関するドキュメント。 +header: + github_docs: 日本 GitHub Docs + sign_up_cta: サインアップ + menu: メニュー +picker: + language_picker_default_text: 言語の選択 + product_picker_default_text: すべての製品 + version_picker_default_text: バージョンを選択する +search: + need_help: ヘルプが必要ですか? + placeholder: GitHub Docs を検索する + search_results_for: 検索結果 + no_content: コンテンツはありません + matches_found: 次の件数の結果が見つかりました + matches_displayed: 一致が表示されました + search_error: 検索を実行しようとして、エラーが発生しました。 + description: GitHub ドキュメントで探す検索語句を入力してください。 + label: GitHub Docs を検索する + n_results: '{n} 件の結果' + one_result: 1 件の結果 +homepage: + explore_by_product: 製品で調べる + version_picker: Version + description: GitHub のどこにいてもお手伝いします。 +toc: + getting_started: 作業の開始 + popular: 基本 + startHere: ここから開始 + whats_new: 新着情報 + videos: ビデオ +pages: + article_version: 記事のバージョン + miniToc: この記事の内容 + all_enterprise_releases: Enterprise Server のすべてのリリース + about_versions: バージョンについて +errors: + oops: 問題が発生しています。 + something_went_wrong: It looks like something went wrong. (問題が発生した可能性があります。) + page_doesnt_exist: 指定されたページは存在しません。 +support: + still_need_help: サポートが必要な場合は、 + contact_support: サポートにお問い合せください + ask_community: GitHub コミュニティで質問する +survey: + able_to_find: このドキュメントは役立ちましたか? + 'yes': はい + 'no': いいえ + comment_yes_label: うまくできていることをお知らせください + comment_no_label: 改善の方法をお知らせください + optional: オプション + required: 必須 + email_placeholder: email@example.com + email_label: さらにお尋ねするためにご連絡してもよろしければ、メール アドレスを入力してください + email_validation: 有効な電子メール アドレスを入力してください + send: Send + feedback: よろしくお願いいたします。 フィードバックを受け取りました。 + not_support: 返信が必要な場合は、サポートにお問い合わせください。 + privacy_policy: プライバシー ポリシー +contribution_cta: + title: これらのドキュメントを素晴らしいものにするのを手伝ってください! + body: GitHub のドキュメントはすべてオープンソースです。 誤りまたは不明瞭な点がありますか? pull request を提出してください。 + button: コントリビューションを行う + to_guidelines: コントリビューションの方法を学ぶ +parameter_table: + body: 本文のパラメーター + default: Default + description: 説明 + enum_description_title: 次のいずれかにできます + headers: ヘッダー + name: 名前 + path: パス パラメーター + query: クエリ パラメーター + required: 必須 + see_preview_notice: プレビューの通知を見る + see_preview_notices: プレビューの通知を見る + type: Type + single_enum_description: 値 +products: + graphql: + reference: + implements: '{{ GraphQLItemTitle }} 実装' + fields: '{{ GraphQLItemTitle }} のフィールド' + arguments: '{{ GraphQLItemTitle }} の引数' + name: 名前 + type: 型 + description: 説明 + input_fields: '{{ GraphQLItemTitle }} の入力フィールド' + return_fields: '{{ GraphQLItemTitle }} の戻り値フィールド' + implemented_by: | + {{ GraphQLItemTitle }} は次で実装されています + values: '{{ GraphQLItemTitle }} の値です。' + possible_types: '{{ GraphQLItemTitle }} に使用できる型' + preview_notice: プレビュー通知 + deprecation_notice: 非推奨の通知 + preview_period: プレビュー期間中、API は通知なしに変更されることがあります。 + overview: + preview_header: このプレビューを切り替えて次のスキーマ メンバーにアクセスするには、`Accept` ヘッダーにカスタムのメディアの種類を指定する必要があります。 + preview_schema_members: プレビューされたスキーマ メンバー + announced: 発表 + updates: 更新プログラム + rest: + banner: + api_versioned: REST API はバージョン管理になりました。 + api_version_info: '詳細については、「API のバージョン管理について」を参照してください。' + ghes_api_versioned: 'サイト管理者が Enterprise Server インスタンスを {{ firstGhesReleaseWithApiVersions.versionTitle }} 以降にアップグレードすると、REST API はバージョン管理されます。 インスタンスのバージョンを検索する方法については、「GitHub Docs のバージョンについて」を参照してください。' + redirect_notice: REST API ドキュメントの一部は最近移動されました。 + redirect_repo: '探しているものが見つからない場合は、新しい {{ newRestPagesLinks }} REST API ページを試してみてください。' + redirect_enterprise: '探しているものが見つからない場合は、{{ actionsPageLink }} REST API ページを試してみてください。' + actions_api_title: Actions + versioning: + about_versions: REST API のバージョンについて + reference: + in: 場所 + description: 説明 + notes: Notes + parameters: '"{{ RESTOperationTitle }}" のパラメーター' + response: '[応答]' + example_response: 応答の例 + status_code: 状態コード + http_status_code: '"{{ RESTOperationTitle }}" の HTTP 応答状態コード' + code_sample: コード サンプル + code_samples: '"{{ RESTOperationTitle }}" のコード サンプル' + preview_notice: '"{{ RESTOperationTitle }}" のプレビュー通知' + preview_notices: '"{{ RESTOperationTitle }}" のプレビュー通知' + preview_header_is_required: このヘッダーは必須です + preview_notice_to_change: この API はプレビュー段階であり、変更される可能性があります + works_with: ' に対応' + api_reference: REST API リファレンス + enum_description_title: 次のいずれかにできます + required: 必須 + headers: ヘッダー + query: クエリ パラメーター + path: パス パラメーター + body: 本文のパラメーター + response_options: + example: 応答の例 + schema: 応答スキーマ + code_sample_options: + ghcli: GitHub CLI + javascript: JavaScript + curl: cURL + webhooks: + action_type_switch_error: Webhook アクションの種類を切り替える際にエラーが発生しました。 + action_type: アクションの種類 + availability: '{{ WebhookName }} の可用性' + webhook_payload_object: '{{ WebhookName }} の Webhook ペイロード オブジェクト' + webhook_payload_example: webhook ペイロードの例 + rephrase_availability: + repository: リポジトリ + organization: 組織 + app: GitHub アプリ + business: Enterprise + marketplace: GitHub Marketplace + sponsors_listing: スポンサー付きアカウント +footer: + all_rights_reserved: All rights reserved + terms: 用語 + privacy: プライバシー + security: Security + product: + heading: 製品 + links: + features: 機能 + security: セキュリティ + enterprise: Enterprise + case_studies: ケース スタディ + pricing: 価格 + resources: リソース + platform: + heading: プラットフォーム + links: + developer_api: 開発者 API + partners: パートナー + atom: Atom + electron: Electron + github_desktop: GitHub Desktop + support: + heading: サポート + links: + help: ヘルプ + community_forum: コミュニティ フォーラム + training: トレーニング + status: 状態 + contact_github: GitHub に連絡 + company: + heading: '[会社]' + links: + about: 詳細 + blog: ブログ + careers: キャリア + press: ショートカット キー + shop: ショップ +product_landing: + quickstart: クイック スタート + reference: リファレンス + overview: 概要 + guides: ガイド + explore_guides: ガイドを調べる + code_examples: コード例 + search_code_examples: 検索コードの例 + search_results_for: 検索結果 + matches_displayed: 一致が表示されました + show_more: さらに表示 + explore_people_and_projects: 人やプロジェクトを調べる + sorry: 検索結果はありません + no_example: 指定されたフィルターが当てはまる例はないようです。 + try_another: 別のフィルターを試すか、ご自分のコードの例を追加してください。 + no_result: 申し訳ありませんが、指定されたフィルターに一致するガイドはありません。 + learn: コード例の追加方法を学ぶ + communities_using_discussions: ディスカッションを使用している GitHub.com 上のコミュニティ + add_your_community: コミュニティを追加する + sponsor_community: GitHub Sponsors コミュニティ + supported_releases: サポートされているリリース + release_notes_for: ' のリリース ノート' + upgrade_from: アップグレード前のバージョン + browse_all_docs: すべてのドキュメントを見る + browse_all: すべて参照 + docs: docs + explore_release_notes: リリース ノートを調べる + view: すべて表示 + view_transcript: ビデオのトランスクリプトを表示 + all_docs: 'すべての {{ title }} ドキュメント' + code_example: + search_button: 検索 + search_examples: '検索コードの例:' +product_guides: + learning_paths_title: '{{ name }} ラーニング パス' + start_path: ラーニング パスの開始 + learning_paths_desc: ラーニング パスは、特定のテーマについてマスターするのに役立つガイド集です。 + more_guides: その他のガイド + load_more: さらにガイドをロード + all_guides_title: 'すべての {{ name }} ガイド' + filter_instructions: これらのコントロールを使ってガイドの一覧をフィルターしてください + filters: + type: Type + topic: トピック + all: すべて + guides_found: + multiple: '{n} 個のガイドが見つかりました' + one: 1 個のガイドが見つかりました + none: ガイドが見つかりませんでした + guide_types: + overview: 概要 + quick_start: クイック スタート + tutorial: チュートリアル + how_to: ハウツー ガイド + reference: リファレンス +learning_track_nav: + prev_guide: 前へ + next_guide: 次へ + more_guides: その他のガイド → + current_progress: 'ラーニング パスの {n} の {i}' +scroll_button: + scroll_to_top: 一番上にスクロールします diff --git a/tests/rendering-fixtures/playwright-rendering.spec.ts b/tests/rendering-fixtures/playwright-rendering.spec.ts index 7958df7ee1b4..7d52113f2501 100644 --- a/tests/rendering-fixtures/playwright-rendering.spec.ts +++ b/tests/rendering-fixtures/playwright-rendering.spec.ts @@ -424,3 +424,59 @@ test.describe('rest API reference pages', () => { await expect(page).toHaveTitle(/GitHub Actions Artifacts - GitHub Docs/) }) }) + +test.describe('translations', () => { + test('view Japanese home page', async ({ page }) => { + await page.goto('/ja') + await expect(page.getByRole('heading', { name: '日本 GitHub Docs' })).toBeVisible() + }) + + test('switch to English from Japanese using banner on home page', async ({ page }) => { + await page.goto('/ja') + await page.getByRole('link', { name: 'English documentation' }).click() + await expect(page).toHaveURL('/en') + await expect(page.getByRole('heading', { name: 'GitHub Docs' })).toBeVisible() + }) + + test('switch to Japanese from English using widget on home page', async ({ page }) => { + await page.goto('/en') + await page.getByRole('button', { name: 'Select language: current language is English' }).click() + await page.getByRole('menuitemradio', { name: '日本語' }).click() + await expect(page).toHaveURL('/ja') + await expect(page.getByRole('heading', { name: '日本 GitHub Docs' })).toBeVisible() + + // Having done this once, should now use a cookie to redirect back to Japanese + await page.goto('/') + await expect(page).toHaveURL('/ja') + }) + + test('switch to English from Japanese using banner on article', async ({ page }) => { + await page.goto('/ja/get-started/quickstart/hello-world') + await expect(page.getByRole('heading', { name: 'こんにちは World' })).toBeVisible() + await page.getByRole('link', { name: 'English documentation' }).click() + await expect(page).toHaveURL('/en/get-started/quickstart/hello-world') + await expect(page.getByRole('heading', { name: 'Hello World' })).toBeVisible() + }) + + test('switch to Japanese from English using widget on article', async ({ page }) => { + await page.goto('/get-started/quickstart/hello-world') + await page.getByRole('button', { name: 'Select language: current language is English' }).click() + await page.getByRole('menuitemradio', { name: '日本語' }).click() + await expect(page).toHaveURL('/ja/get-started/quickstart/hello-world') + await expect(page.getByRole('heading', { name: 'こんにちは World' })).toBeVisible() + + // Having done this once, should now use a cookie to redirect + // back to Japanese. + // Playwright will cache this redirect, so we need to add something + // to "cache bust" the URL + const cb = `?cb=${Math.random()}` + await page.goto('/get-started/quickstart/hello-world' + cb) + await expect(page).toHaveURL('/ja/get-started/quickstart/hello-world' + cb) + + // If you go, with the Japanese cookie, to the English page directly, + // it will offer a link to the Japanese URL in a banner. + await page.goto('/en/get-started/quickstart/hello-world') + await page.getByRole('link', { name: 'Japanese' }).click() + await expect(page).toHaveURL('/ja/get-started/quickstart/hello-world') + }) +}) diff --git a/tests/rendering-fixtures/translations.js b/tests/rendering-fixtures/translations.js new file mode 100644 index 000000000000..09c6e32bac95 --- /dev/null +++ b/tests/rendering-fixtures/translations.js @@ -0,0 +1,53 @@ +import { expect } from '@jest/globals' + +import { TRANSLATIONS_FIXTURE_ROOT } from '../../lib/constants.js' +import { getDOM } from '../helpers/e2etest.js' + +if (!TRANSLATIONS_FIXTURE_ROOT) { + let msg = 'You have to set TRANSLATIONS_FIXTURE_ROOT to run this test.' + msg += ' Add TRANSLATIONS_FIXTURE_ROOT=tests/fixtures/translations' + throw new Error(msg) +} + +describe('translations', () => { + test('home page', async () => { + const $ = await getDOM('/ja') + const h1 = $('h1').text() + // You gotta know your tests/fixtures/translations/ja-jp/data/ui.yml + expect(h1).toBe('日本 GitHub Docs') + + // The header banner mentions something about + // "For the most up-to-date content, see the English version." + const notification = $('[data-testid="header-notification"]') + expect(notification.length).toBe(1) + const toEnglishDoc = notification.find('a#to-english-doc') + expect(toEnglishDoc.text()).toBe('English documentation') + }) + + test('hello world', async () => { + const $ = await getDOM('/ja/get-started/quickstart/hello-world') + const h1 = $('h1').text() + expect(h1).toBe('こんにちは World') + }) + + test('internal links get prefixed with /ja', async () => { + const $ = await getDOM('/ja/get-started/quickstart/link-rewriting') + const links = $('#article-contents a[href]') + const jaLinks = links.filter((i, element) => $(element).attr('href').startsWith('/ja')) + const enLinks = links.filter((i, element) => $(element).attr('href').startsWith('/en')) + expect(jaLinks.length).toBe(2) + expect(enLinks.length).toBe(0) + }) + + test('internal links with AUTOTITLE resolves', async () => { + const $ = await getDOM('/ja/get-started/foo/autotitling') + const links = $('#article-contents a[href]') + links.each((i, element) => { + if ($(element).attr('href').includes('/ja/get-started/quickstart/hello-world')) { + expect($(element).text()).toBe('こんにちは World') + } + }) + // There are 4 links on the `autotitling.md` content. + expect.assertions(4) + }) +})