From 428a7c2c9165b43c0343e293acd143e0bdac58b4 Mon Sep 17 00:00:00 2001 From: Miki Date: Fri, 4 Oct 2024 18:05:53 -0700 Subject: [PATCH] Add i18n checks to PR workflows (#8411) * Ignore missing `formats` while checking locale files Also: * Add help text and description to `i18n-check` * Fix malformed translations Signed-off-by: Miki * Add i18n checks to PR workflows Signed-off-by: Miki * Changeset file for PR #8411 created/updated --------- Signed-off-by: Miki Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> --- .github/workflows/build_and_test_workflow.yml | 59 +++++++++++++++---- changelogs/fragments/8411.yml | 9 +++ package.json | 2 + src/dev/i18n/integrate_locale_files.ts | 7 ++- src/dev/i18n/tasks/check_compatibility.ts | 13 +++- src/dev/run_i18n_check.ts | 16 ++++- src/translations/de-DE.json | 4 +- src/translations/es-ES.json | 2 +- src/translations/fr-FR.json | 12 ++-- src/translations/ko-KR.json | 4 +- src/translations/tr-TR.json | 4 +- src/translations/zh-CN.json | 2 +- 12 files changed, 103 insertions(+), 31 deletions(-) create mode 100644 changelogs/fragments/8411.yml diff --git a/.github/workflows/build_and_test_workflow.yml b/.github/workflows/build_and_test_workflow.yml index 728722e952db..f44a745f9871 100644 --- a/.github/workflows/build_and_test_workflow.yml +++ b/.github/workflows/build_and_test_workflow.yml @@ -34,7 +34,7 @@ env: NODE_OPTIONS: '--max-old-space-size=6144 --dns-result-order=ipv4first' jobs: - build-lint-test: + build-test: name: Build and Verify on ${{ matrix.name }} (ciGroup${{ matrix.group }}) strategy: fail-fast: false @@ -104,18 +104,6 @@ jobs: if: matrix.os == 'windows-latest' run: yarn osd bootstrap || yarn osd bootstrap - - name: Run linter - # ciGroup 1 of unit-tests is shorter and Linux is faster - if: matrix.group == 1 && matrix.os == 'ubuntu-latest' - id: linter - run: yarn lint - - - name: Validate NOTICE file - # ciGroup 1 of unit-tests is shorter and Linux is faster - if: matrix.group == 1 && matrix.os == 'ubuntu-latest' - id: notice-validate - run: yarn notice:validate - - name: Run unit tests group ${{ matrix.group }} with coverage id: unit-tests run: yarn test:jest:ci:coverage --ci-group=${{ matrix.group }} @@ -140,6 +128,51 @@ jobs: id: integration-tests run: yarn test:jest_integration:ci + lint-and-validate: + name: Lint and validate + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version-file: '.nvmrc' + registry-url: 'https://registry.npmjs.org' + + - name: Setup Yarn + run: | + npm uninstall -g yarn + npm i -g yarn@1.22.10 + yarn config set network-timeout 1000000 -g + + - name: Configure Yarn Cache + run: echo "YARN_CACHE_LOCATION=$(yarn cache dir)" >> $GITHUB_ENV + + - name: Initialize Yarn Cache + uses: actions/cache@v3 + with: + path: ${{ env.YARN_CACHE_LOCATION }} + key: yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + yarn- + + - name: Run bootstrap + run: yarn osd bootstrap + + - name: Run linter + id: linter + run: yarn lint + + - name: Validate NOTICE file + id: notice-validate + run: yarn notice:validate + + - name: Check i18n + id: i18n-check + run: yarn i18n:check + functional-tests: name: Run functional tests on ${{ matrix.name }} (ciGroup${{ matrix.group }}) strategy: diff --git a/changelogs/fragments/8411.yml b/changelogs/fragments/8411.yml new file mode 100644 index 000000000000..56940f03c677 --- /dev/null +++ b/changelogs/fragments/8411.yml @@ -0,0 +1,9 @@ +infra: +- Add i18n checks to PR workflows ([#8411](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8411)) + +feat: +- Ignore missing `formats` while checking locale files ([#8411](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8411)) +- Add help text and description to `i18n-check` ([#8411](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8411)) + +fix: +- Fix malformed translations ([#8411](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8411)) \ No newline at end of file diff --git a/package.json b/package.json index d5e32f8f3a42..ab30583b4d21 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,8 @@ "lint": "yarn run lint:es && yarn run lint:style", "lint:es": "scripts/use_node scripts/eslint", "lint:style": "scripts/use_node scripts/stylelint", + "i18n:check": "scripts/use_node scripts/i18n_check --ignore-missing --ignore-unused", + "i18n:extract": "scripts/use_node scripts/i18n_extract.js", "makelogs": "scripts/use_node scripts/makelogs", "uiFramework:compileCss": "cd packages/osd-ui-framework && yarn compileCss", "osd:watch": "scripts/use_node scripts/opensearch_dashboards --dev --logging.json=false", diff --git a/src/dev/i18n/integrate_locale_files.ts b/src/dev/i18n/integrate_locale_files.ts index 0f3a74a82faa..55b09fcc2985 100644 --- a/src/dev/i18n/integrate_locale_files.ts +++ b/src/dev/i18n/integrate_locale_files.ts @@ -56,6 +56,7 @@ export interface IntegrateOptions { ignoreIncompatible: boolean; ignoreUnused: boolean; ignoreMissing: boolean; + ignoreMissingFormats?: boolean; config: I18nConfig; log: ToolingLog; } @@ -211,7 +212,11 @@ export async function integrateLocaleFiles( ) { const localizedMessages = JSON.parse((await readFileAsync(options.sourceFileName)).toString()); if (!localizedMessages.formats) { - throw createFailError(`Locale file should contain formats object.`); + if (options.ignoreMissingFormats) { + options.log.warning('Missing formats object ignored'); + } else { + throw createFailError(`Locale file should contain formats object.`); + } } const localizedMessagesMap: LocalizedMessageMap = new Map( diff --git a/src/dev/i18n/tasks/check_compatibility.ts b/src/dev/i18n/tasks/check_compatibility.ts index 7af9ef5604a9..eb6ceb5ffc79 100644 --- a/src/dev/i18n/tasks/check_compatibility.ts +++ b/src/dev/i18n/tasks/check_compatibility.ts @@ -37,6 +37,7 @@ export interface I18nFlags { ignoreIncompatible: boolean; ignoreUnused: boolean; ignoreMissing: boolean; + ignoreMissingFormats: boolean; } export function checkCompatibility( @@ -47,16 +48,24 @@ export function checkCompatibility( if (!config) { throw new Error('Config is missing'); } - const { fix, ignoreIncompatible, ignoreUnused, ignoreMalformed, ignoreMissing } = flags; + const { + fix, + ignoreIncompatible, + ignoreUnused, + ignoreMalformed, + ignoreMissing, + ignoreMissingFormats, + } = flags; return config.translations.map((translationsPath) => ({ task: async ({ messages }: { messages: Map }) => { - // If `fix` is set we should try apply all possible fixes and override translations file. + // If `fix` is set we should try to apply all possible fixes and override translations file. await integrateLocaleFiles(messages, { dryRun: !fix, ignoreIncompatible: fix || ignoreIncompatible, ignoreUnused: fix || ignoreUnused, ignoreMissing: fix || ignoreMissing, ignoreMalformed: fix || ignoreMalformed, + ignoreMissingFormats, sourceFileName: translationsPath, targetFileName: fix ? translationsPath : undefined, config, diff --git a/src/dev/run_i18n_check.ts b/src/dev/run_i18n_check.ts index 08ff3a6f09b1..2dc9a6530068 100644 --- a/src/dev/run_i18n_check.ts +++ b/src/dev/run_i18n_check.ts @@ -45,6 +45,7 @@ import { DEFAULT_DIRS_WITH_RC_FILES } from './i18n/constants'; const skipOnNoTranslations = (context: ListrContext) => !context.config?.translations?.length && 'No translations found.'; + run( async ({ flags: { @@ -54,6 +55,7 @@ run( 'ignore-unused': ignoreUnused, 'include-config': includeConfig, 'ignore-untracked': ignoreUntracked, + 'ignore-missing-formats': ignoreMissingFormats, fix = false, path, }, @@ -121,11 +123,13 @@ run( ignoreIncompatible: !!ignoreIncompatible, ignoreUnused: !!ignoreUnused, ignoreMissing: !!ignoreMissing, + // By default ignore missing formats + ignoreMissingFormats: ignoreMissingFormats !== false, fix, }, log ), - { exitOnError: true } + { exitOnError: false } ); }, }, @@ -154,6 +158,16 @@ run( flags: { allowUnexpected: true, guessTypesForUnexpectedFlags: true, + help: ` + --ignore-incompatible Ignore mismatched keys in values and tokens in translations + --ignore-malformed Ignore malformed ICU format usages + --ignore-missing Ignore missing translations in locale files + --ignore-unused Ignore unused translations in locale files + --ignore-untracked Ignore untracked files with i18n labels + --ignore-missing-formats Ignore missing 'formats' key in locale files + (default: true, use --ignore-missing-formats=false to disable) + `, }, + description: 'Checks i18n usage in code and validates translation files', } ); diff --git a/src/translations/de-DE.json b/src/translations/de-DE.json index 108c627708d9..8b79b35cf288 100644 --- a/src/translations/de-DE.json +++ b/src/translations/de-DE.json @@ -94,7 +94,7 @@ "data.filter.filterBar.indexPatternSelectPlaceholder": "Ein Indexmuster auswählen", "data.filter.filterBar.labelErrorInfo": "Indexmuster {indexPattern} nicht gefunden", "data.filter.filterBar.labelErrorText": "Fehler", - "data.filter.filterBar.labelWarningInfo": "Feld {FieldName} in der aktuellen Ansicht nicht vorhanden", + "data.filter.filterBar.labelWarningInfo": "Feld {fieldName} in der aktuellen Ansicht nicht vorhanden", "data.filter.filterBar.labelWarningText": "WARNUNG", "data.filter.filterBar.moreFilterActionsMessage": "Filter: {innerText}. Diese Option für weitere Filteraktionen wählen.", "data.filter.filterBar.negatedFilterPrefix": "NICHT ", @@ -186,7 +186,7 @@ "dashboard.listing.createButtonText": "Erstellen Sie", "dashboard.listing.createNewDashboard.combineDataViewFromOpenSearchDashboardsAppDescription": "Sie können Datenansichten aus jeder OpenSearch-Dashboards-App in einem Dashboard kombinieren und alles an einem Ort sehen.", "dashboard.listing.createNewDashboard.createButtonLabel": "Neues Dashboard erstellen", - "dashboard.listing.createNewDashboard.newToOpenSearchDashboardsDescription": "Neu bei OpenSearch Dashboards? Öffnen Sie {SampleDataInstallLink} für einen Test.", + "dashboard.listing.createNewDashboard.newToOpenSearchDashboardsDescription": "Neu bei OpenSearch Dashboards? Öffnen Sie {sampleDataInstallLink} für einen Test.", "dashboard.listing.createNewDashboard.sampleDataInstallLinkText": "Installieren Sie einige Beispieldaten", "dashboard.listing.createNewDashboard.title": "Erstellen Sie Ihr erstes Dashboard", "dashboard.listing.dashboardsTitle": "Dashboards", diff --git a/src/translations/es-ES.json b/src/translations/es-ES.json index 9a8b97cea411..e2c40d7f13c4 100644 --- a/src/translations/es-ES.json +++ b/src/translations/es-ES.json @@ -186,7 +186,7 @@ "dashboard.listing.createButtonText": "Cree", "dashboard.listing.createNewDashboard.combineDataViewFromOpenSearchDashboardsAppDescription": "Puede combinar las vistas de datos de cualquier aplicación de OpenSearch Dashboards en un solo panel y ver todo en un mismo lugar.", "dashboard.listing.createNewDashboard.createButtonLabel": "Crear un panel", - "dashboard.listing.createNewDashboard.newToOpenSearchDashboardsDescription": "¿Es la primera vez que usa OpenSearch Dashboards? {SampleDataInstallLink} para probarlo.", + "dashboard.listing.createNewDashboard.newToOpenSearchDashboardsDescription": "¿Es la primera vez que usa OpenSearch Dashboards? {sampleDataInstallLink} para probarlo.", "dashboard.listing.createNewDashboard.sampleDataInstallLinkText": "Instale algunos datos de muestra", "dashboard.listing.createNewDashboard.title": "Cree su primer panel", "dashboard.listing.dashboardsTitle": "Paneles", diff --git a/src/translations/fr-FR.json b/src/translations/fr-FR.json index 9397f38cb5e9..60fc652eedb7 100644 --- a/src/translations/fr-FR.json +++ b/src/translations/fr-FR.json @@ -92,7 +92,7 @@ "data.filter.filterBar.filterItemBadgeIconAriaLabel": "Supprimer", "data.filter.filterBar.includeFilterButtonLabel": "Inclure les résultats", "data.filter.filterBar.indexPatternSelectPlaceholder": "Sélectionner un modèle d’index", - "data.filter.filterBar.labelErrorInfo": "Modèle d’index {IndexPattern} introuvable", + "data.filter.filterBar.labelErrorInfo": "Modèle d’index {indexPattern} introuvable", "data.filter.filterBar.labelErrorText": "Erreur", "data.filter.filterBar.labelWarningInfo": "Le champ {fieldName} n’existe pas dans la vue actuelle", "data.filter.filterBar.labelWarningText": "Avertissement", @@ -147,9 +147,9 @@ "dashboard.addExistingVisualizationLinkText": "Ajouter un existant", "dashboard.addNewVisualizationText": "ou nouvel objet sur ce tableau de bord", "dashboard.addPanel.noMatchingObjectsMessage": "Aucun objet correspondant n’a été trouvé.", - "dashboard.addPanel.savedObjectAddedToContainerSuccessMessageTitle": "{SavedObjectName} a été ajouté", + "dashboard.addPanel.savedObjectAddedToContainerSuccessMessageTitle": "{savedObjectName} a été ajouté", "dashboard.addVisualizationLinkAriaLabel": "Ajouter une visualisation existante", - "dashboard.attributeService.saveToLibraryError": "Une erreur s’est produite lors de l’enregistrement. Erreur : {ErrorMessage}", + "dashboard.attributeService.saveToLibraryError": "Une erreur s’est produite lors de l’enregistrement. Erreur : {errorMessage}", "dashboard.changeViewModeConfirmModal.cancelButtonLabel": "Poursuivre l’édition", "dashboard.changeViewModeConfirmModal.confirmButtonLabel": "Annuler les modifications", "dashboard.changeViewModeConfirmModal.discardChangesDescription": "Une fois que vous avez ignoré vos modifications, il n’est plus possible de les récupérer.", @@ -163,7 +163,7 @@ "dashboard.dashboardGrid.toast.unableToLoadDashboardDangerMessage": "Impossible de charger le tableau de bord.", "dashboard.dashboardListingDeleteErrorTitle": "Erreur de suppression du tableau de bord", "dashboard.dashboardPageTitle": "Tableaux de bord", - "dashboard.dashboardWasNotSavedDangerMessage": "Le tableau de bord « {dashTitle} » n’a pas été enregistré. Erreur : {ErrorMessage}", + "dashboard.dashboardWasNotSavedDangerMessage": "Le tableau de bord « {dashTitle} » n’a pas été enregistré. Erreur : {errorMessage}", "dashboard.dashboardWasSavedSuccessMessage": "Le tableau de bord « {dashTitle} » a été enregistré", "dashboard.embedUrlParamExtension.filterBar": "Barre de filtrage", "dashboard.embedUrlParamExtension.include": "Inclure", @@ -186,7 +186,7 @@ "dashboard.listing.createButtonText": "Créez", "dashboard.listing.createNewDashboard.combineDataViewFromOpenSearchDashboardsAppDescription": "Vous pouvez combiner les vues de données de n’importe quelle application OpenSearch Dashboards dans un seul tableau de bord et tout voir au même endroit.", "dashboard.listing.createNewDashboard.createButtonLabel": "Créer un nouveau tableau de bord", - "dashboard.listing.createNewDashboard.newToOpenSearchDashboardsDescription": "Vous êtes nouveau sur OpenSearch Dashboards ? {SampleDataInstallLink} pour faire un essai routier.", + "dashboard.listing.createNewDashboard.newToOpenSearchDashboardsDescription": "Vous êtes nouveau sur OpenSearch Dashboards ? {sampleDataInstallLink} pour faire un essai routier.", "dashboard.listing.createNewDashboard.sampleDataInstallLinkText": "Installer quelques exemples de données", "dashboard.listing.createNewDashboard.title": "Créez votre premier tableau de bord", "dashboard.listing.dashboardsTitle": "Tableaux de bord", @@ -228,7 +228,7 @@ "dashboard.topNav.cloneModal.cloneDashboardModalHeaderTitle": "Tableau de bord cloné", "dashboard.topNav.cloneModal.confirmButtonLabel": "Confirmer le clone", "dashboard.topNav.cloneModal.confirmCloneDescription": "Confirmer le clone", - "dashboard.topNav.cloneModal.dashboardExistsDescription": "Cliquer sur {ConfirmClone} pour cloner le tableau de bord avec le titre dupliqué.", + "dashboard.topNav.cloneModal.dashboardExistsDescription": "Cliquer sur {confirmClone} pour cloner le tableau de bord avec le titre dupliqué.", "dashboard.topNav.cloneModal.dashboardExistsTitle": "Un tableau de bord intitulé {newDashboardName} existe déjà.", "dashboard.topNav.cloneModal.enterNewNameForDashboardDescription": "Entrez un nouveau nom pour votre tableau de bord.", "dashboard.topNav.editSwitchLabel": "Modifier", diff --git a/src/translations/ko-KR.json b/src/translations/ko-KR.json index b37f6622b1cd..23c236429209 100644 --- a/src/translations/ko-KR.json +++ b/src/translations/ko-KR.json @@ -87,12 +87,12 @@ "data.filter.filterBar.editFilterButtonLabel": "필터 편집", "data.filter.filterBar.enableFilterButtonLabel": "다시 활성화", "data.filter.filterBar.excludeFilterButtonLabel": "결과 제외", - "data.filter.filterBar.fieldNotFound": "{IndexPattern} 인덱스 패턴에서 {key} 필드를 찾을 수 없음", + "data.filter.filterBar.fieldNotFound": "{indexPattern} 인덱스 패턴에서 {key} 필드를 찾을 수 없음", "data.filter.filterBar.filterItemBadgeAriaLabel": "필터 작업", "data.filter.filterBar.filterItemBadgeIconAriaLabel": "삭제", "data.filter.filterBar.includeFilterButtonLabel": "결과 포함", "data.filter.filterBar.indexPatternSelectPlaceholder": "인덱스 패턴 선택", - "data.filter.filterBar.labelErrorInfo": "{IndexPattern} 인덱스 패턴을 찾을 수 없음", + "data.filter.filterBar.labelErrorInfo": "{indexPattern} 인덱스 패턴을 찾을 수 없음", "data.filter.filterBar.labelErrorText": "오류", "data.filter.filterBar.labelWarningInfo": "{fieldName} 필드가 현재 보기에 없음", "data.filter.filterBar.labelWarningText": "경고", diff --git a/src/translations/tr-TR.json b/src/translations/tr-TR.json index 296fc7fd3d9d..bf507e037edf 100644 --- a/src/translations/tr-TR.json +++ b/src/translations/tr-TR.json @@ -186,7 +186,7 @@ "dashboard.listing.createButtonText": "Oluştur", "dashboard.listing.createNewDashboard.combineDataViewFromOpenSearchDashboardsAppDescription": "Herhangi bir OpenSearch Dashboards uygulamasındaki veri görünümlerini tek bir panoda birleştirebilir ve her şeyi tek bir yerde görebilirsiniz.", "dashboard.listing.createNewDashboard.createButtonLabel": "Yeni pano oluştur", - "dashboard.listing.createNewDashboard.newToOpenSearchDashboardsDescription": "OpenSearch Dashboards'da yeni misiniz? Deneme yapmak için {SampleDataInstallLink} bağlantısına tıklayın.", + "dashboard.listing.createNewDashboard.newToOpenSearchDashboardsDescription": "OpenSearch Dashboards'da yeni misiniz? Deneme yapmak için {sampleDataInstallLink} bağlantısına tıklayın.", "dashboard.listing.createNewDashboard.sampleDataInstallLinkText": "Bazı örnek verileri yükleyin", "dashboard.listing.createNewDashboard.title": "İlk panonuzu oluşturun", "dashboard.listing.dashboardsTitle": "Panolar", @@ -228,7 +228,7 @@ "dashboard.topNav.cloneModal.cloneDashboardModalHeaderTitle": "Panoyu kopyala", "dashboard.topNav.cloneModal.confirmButtonLabel": "Kopyalamayı onayla", "dashboard.topNav.cloneModal.confirmCloneDescription": "Kopyalamayı onayla", - "dashboard.topNav.cloneModal.dashboardExistsDescription": "Yinelenen başlıklı panoyu kopyalamak için {ConfirmClone} öğesine tıklayın.", + "dashboard.topNav.cloneModal.dashboardExistsDescription": "Yinelenen başlıklı panoyu kopyalamak için {confirmClone} öğesine tıklayın.", "dashboard.topNav.cloneModal.dashboardExistsTitle": "{newDashboardName} başlıklı bir pano zaten var.", "dashboard.topNav.cloneModal.enterNewNameForDashboardDescription": "Lütfen panonuz için yeni bir ad girin.", "dashboard.topNav.editSwitchLabel": "Düzenle", diff --git a/src/translations/zh-CN.json b/src/translations/zh-CN.json index 715981a4a4ca..df63527d3299 100644 --- a/src/translations/zh-CN.json +++ b/src/translations/zh-CN.json @@ -163,7 +163,7 @@ "dashboard.dashboardGrid.toast.unableToLoadDashboardDangerMessage": "无法加载控制面板。", "dashboard.dashboardListingDeleteErrorTitle": "删除控制面板时出错", "dashboard.dashboardPageTitle": "控制面板", - "dashboard.dashboardWasNotSavedDangerMessage": "控制面板“{dashTitle}”未保存。错误: ", + "dashboard.dashboardWasNotSavedDangerMessage": "控制面板“{dashTitle}”未保存。错误: {errorMessage}", "dashboard.dashboardWasSavedSuccessMessage": "控制面板“{dashTitle}”已保存", "dashboard.embedUrlParamExtension.filterBar": "筛选栏", "dashboard.embedUrlParamExtension.include": "包括",