diff --git a/CHANGELOG.md b/CHANGELOG.md index ba0570bd4ce3..611454140a58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) ### 🛠 Maintenance +- Remove angular html extractor ([#4680](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4680)) - Removes `minimatch` manual resolution ([#3019](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3019)) - Upgrade `vega-lite` dependency from `4.17.0` to `^5.6.0` ([#3076](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3076)). Backwards-compatible version included in v2.5.0 release. - Bump `js-yaml` from `3.14.0` to `4.1.0` ([#3770](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3770)) diff --git a/src/dev/i18n/__snapshots__/extract_default_translations.test.js.snap b/src/dev/i18n/__snapshots__/extract_default_translations.test.js.snap index b19b366a8db7..68ed0433f1ae 100644 --- a/src/dev/i18n/__snapshots__/extract_default_translations.test.js.snap +++ b/src/dev/i18n/__snapshots__/extract_default_translations.test.js.snap @@ -30,13 +30,6 @@ Array [ "message": "Message 4", }, ], - Array [ - "plugin_1.id_7", - Object { - "description": undefined, - "message": "Message 7", - }, - ], ] `; diff --git a/src/dev/i18n/extract_default_translations.js b/src/dev/i18n/extract_default_translations.js index e99c733bcb98..3581e6e6982f 100644 --- a/src/dev/i18n/extract_default_translations.js +++ b/src/dev/i18n/extract_default_translations.js @@ -30,7 +30,7 @@ import path from 'path'; -import { extractHtmlMessages, extractCodeMessages } from './extractors'; +import { extractCodeMessages } from './extractors'; import { globAsync, readFileAsync, normalizePath } from './utils'; import { createFailError, isFailError } from '@osd/dev-utils'; @@ -81,7 +81,7 @@ export async function matchEntriesWithExctractors(inputPath, options = {}) { '**/*.d.ts', ].concat(additionalIgnore); - const entries = await globAsync('*.{js,jsx,ts,tsx,html}', { + const entries = await globAsync('*.{js,jsx,ts,tsx}', { cwd: inputPath, matchBase: true, ignore, @@ -89,25 +89,13 @@ export async function matchEntriesWithExctractors(inputPath, options = {}) { absolute, }); - const { htmlEntries, codeEntries } = entries.reduce( - (paths, entry) => { - const resolvedPath = path.resolve(inputPath, entry); + const codeEntries = entries.reduce((paths, entry) => { + const resolvedPath = path.resolve(inputPath, entry); - if (resolvedPath.endsWith('.html')) { - paths.htmlEntries.push(resolvedPath); - } else { - paths.codeEntries.push(resolvedPath); - } - - return paths; - }, - { htmlEntries: [], codeEntries: [] } - ); - - return [ - [htmlEntries, extractHtmlMessages], - [codeEntries, extractCodeMessages], - ]; + paths.push(resolvedPath); + return paths; + }, []); + return [[codeEntries, extractCodeMessages]]; } export async function extractMessagesFromPathToMap(inputPath, targetMap, config, reporter) { diff --git a/src/dev/i18n/extractors/__snapshots__/html.test.js.snap b/src/dev/i18n/extractors/__snapshots__/html.test.js.snap deleted file mode 100644 index c7cd77b23f1f..000000000000 --- a/src/dev/i18n/extractors/__snapshots__/html.test.js.snap +++ /dev/null @@ -1,77 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`dev/i18n/extractors/html extracts default messages from HTML 1`] = ` -Array [ - Array [ - "osd.dashboard.id-1", - Object { - "description": "Message description 1", - "message": "Message text 1 {value}", - }, - ], - Array [ - "osd.dashboard.id-2", - Object { - "description": undefined, - "message": "Message text 2", - }, - ], - Array [ - "osd.dashboard.id-3", - Object { - "description": "Message description 3", - "message": "Message text 3", - }, - ], -] -`; - -exports[`dev/i18n/extractors/html extracts default messages from HTML with one-time binding 1`] = ` -Array [ - Array [ - "osd.id", - Object { - "description": undefined, - "message": "Message text with {value}", - }, - ], -] -`; - -exports[`dev/i18n/extractors/html extracts message from i18n filter in interpolating directive 1`] = ` -Array [ - Array [ - "namespace.messageId", - Object { - "description": undefined, - "message": "Message", - }, - ], -] -`; - -exports[`dev/i18n/extractors/html throws on empty i18n-id 1`] = ` -Array [ - Array [ - [Error: Empty "i18n-id" value in angular directive is not allowed.], - ], -] -`; - -exports[`dev/i18n/extractors/html throws on i18n filter usage in complex angular expression 1`] = ` -Array [ - Array [ - [Error: Couldn't parse angular i18n expression: -Missing semicolon. (1:5): - mode as ('metricVis.colorModes.' + mode], - ], -] -`; - -exports[`dev/i18n/extractors/html throws on missing i18n-default-message attribute 1`] = ` -Array [ - Array [ - [Error: Empty defaultMessage in angular directive is not allowed ("message-id").], - ], -] -`; diff --git a/src/dev/i18n/extractors/html.js b/src/dev/i18n/extractors/html.js deleted file mode 100644 index a145b2feaf1a..000000000000 --- a/src/dev/i18n/extractors/html.js +++ /dev/null @@ -1,274 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Any modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import cheerio from 'cheerio'; -import { parse } from '@babel/parser'; -import { isObjectExpression, isStringLiteral } from '@babel/types'; - -import { - isPropertyWithKey, - formatHTMLString, - formatJSString, - traverseNodes, - checkValuesProperty, - createParserErrorMessage, - extractMessageValueFromNode, - extractValuesKeysFromNode, - extractDescriptionValueFromNode, -} from '../utils'; -import { DEFAULT_MESSAGE_KEY, DESCRIPTION_KEY, VALUES_KEY } from '../constants'; -import { createFailError, isFailError } from '@osd/dev-utils'; - -/** - * Find all substrings of "{{ any text }}" pattern allowing '{' and '}' chars in single quote strings - * - * Example: `{{ ::'message.id' | i18n: { defaultMessage: 'Message with {{curlyBraces}}' } }}` - */ -const ANGULAR_EXPRESSION_REGEX = /{{([^{}]|({([^']|('([^']|(\\'))*'))*?}))*}}+/g; - -const LINEBREAK_REGEX = /\n/g; -const I18N_FILTER_MARKER = '| i18n: '; - -function parseExpression(expression) { - let ast; - - try { - ast = parse(`+${expression}`.replace(LINEBREAK_REGEX, ' ')); - } catch (error) { - if (error instanceof SyntaxError) { - const errorWithContext = createParserErrorMessage(` ${expression}`, error); - throw createFailError(`Couldn't parse angular i18n expression:\n${errorWithContext}`); - } - } - - return ast; -} - -/** - * Extract default message from an angular filter expression argument - * @param {string} expression JavaScript code containing a filter object - * @param {string} messageId id of the message - * @returns {{ message?: string, description?: string, valuesKeys: string[]] }} - */ -function parseFilterObjectExpression(expression, messageId) { - const ast = parseExpression(expression); - const objectExpressionNode = [...traverseNodes(ast.program.body)].find((node) => - isObjectExpression(node) - ); - - if (!objectExpressionNode) { - return {}; - } - - const [messageProperty, descriptionProperty, valuesProperty] = [ - DEFAULT_MESSAGE_KEY, - DESCRIPTION_KEY, - VALUES_KEY, - ].map((key) => - objectExpressionNode.properties.find((property) => isPropertyWithKey(property, key)) - ); - - const message = messageProperty - ? formatJSString(extractMessageValueFromNode(messageProperty.value, messageId)) - : undefined; - - const description = descriptionProperty - ? formatJSString(extractDescriptionValueFromNode(descriptionProperty.value, messageId)) - : undefined; - - const valuesKeys = valuesProperty - ? extractValuesKeysFromNode(valuesProperty.value, messageId) - : []; - - return { message, description, valuesKeys }; -} - -function parseIdExpression(expression) { - const ast = parseExpression(expression); - const stringNode = [...traverseNodes(ast.program.body)].find((node) => isStringLiteral(node)); - - if (!stringNode) { - throw createFailError(`Message id should be a string literal, but got: \n${expression}`); - } - - return stringNode ? formatJSString(stringNode.value) : null; -} - -function trimCurlyBraces(string) { - if (string.startsWith('{{') && string.endsWith('}}')) { - return string.slice(2, -2).trim(); - } - - return string; -} - -/** - * Removes one-time binding operator `::` from the start of a string. - * - * Example: `::'id' | i18n: { defaultMessage: 'Message' }` - * @param {string} string string to trim - */ -function trimOneTimeBindingOperator(string) { - if (string.startsWith('::')) { - return string.slice(2); - } - - return string; -} - -function* extractExpressions(htmlContent) { - const elements = cheerio.load(htmlContent)('*').toArray(); - - for (const element of elements) { - for (const node of element.children) { - if (node.type === 'text') { - yield* (node.data.match(ANGULAR_EXPRESSION_REGEX) || []) - .filter((expression) => expression.includes(I18N_FILTER_MARKER)) - .map(trimCurlyBraces); - } - } - - for (const attribute of Object.values(element.attribs)) { - if (attribute.includes(I18N_FILTER_MARKER)) { - yield trimCurlyBraces(attribute); - } - } - } -} - -function* getFilterMessages(htmlContent, reporter) { - for (const expression of extractExpressions(htmlContent)) { - const filterStart = expression.indexOf(I18N_FILTER_MARKER); - - const idExpression = trimOneTimeBindingOperator(expression.slice(0, filterStart).trim()); - const filterObjectExpression = expression.slice(filterStart + I18N_FILTER_MARKER.length).trim(); - - try { - if (!filterObjectExpression || !idExpression) { - throw createFailError(`Cannot parse i18n filter expression: ${expression}`); - } - - const messageId = parseIdExpression(idExpression); - - if (!messageId) { - throw createFailError('Empty "id" value in angular filter expression is not allowed.'); - } - - const { message, description, valuesKeys } = parseFilterObjectExpression( - filterObjectExpression, - messageId - ); - - if (!message) { - throw createFailError( - `Empty defaultMessage in angular filter expression is not allowed ("${messageId}").` - ); - } - - checkValuesProperty(valuesKeys, message, messageId); - - yield [messageId, { message, description }]; - } catch (error) { - if (!isFailError(error)) { - throw error; - } - - reporter.report(error); - } - } -} - -function* getDirectiveMessages(htmlContent, reporter) { - const $ = cheerio.load(htmlContent); - - const elements = $('[i18n-id]') - .map((idx, el) => { - const $el = $(el); - - return { - id: $el.attr('i18n-id'), - defaultMessage: $el.attr('i18n-default-message'), - description: $el.attr('i18n-description'), - values: $el.attr('i18n-values'), - }; - }) - .toArray(); - - for (const element of elements) { - const messageId = formatHTMLString(element.id); - if (!messageId) { - reporter.report( - createFailError('Empty "i18n-id" value in angular directive is not allowed.') - ); - continue; - } - - const message = formatHTMLString(element.defaultMessage); - if (!message) { - reporter.report( - createFailError( - `Empty defaultMessage in angular directive is not allowed ("${messageId}").` - ) - ); - continue; - } - - try { - if (element.values) { - const ast = parseExpression(element.values); - const valuesObjectNode = [...traverseNodes(ast.program.body)].find((node) => - isObjectExpression(node) - ); - const valuesKeys = extractValuesKeysFromNode(valuesObjectNode); - - checkValuesProperty(valuesKeys, message, messageId); - } else { - checkValuesProperty([], message, messageId); - } - - yield [ - messageId, - { message, description: formatHTMLString(element.description) || undefined }, - ]; - } catch (error) { - if (!isFailError(error)) { - throw error; - } - - reporter.report(error); - } - } -} - -export function* extractHtmlMessages(buffer, reporter) { - const content = buffer.toString(); - yield* getDirectiveMessages(content, reporter); - yield* getFilterMessages(content, reporter); -} diff --git a/src/dev/i18n/extractors/html.test.js b/src/dev/i18n/extractors/html.test.js deleted file mode 100644 index ffb00207f193..000000000000 --- a/src/dev/i18n/extractors/html.test.js +++ /dev/null @@ -1,125 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Any modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { extractHtmlMessages } from './html'; - -const htmlSourceBuffer = Buffer.from(` -
-
-

-
-
- {{ 'osd.dashboard.id-2' | i18n: { defaultMessage: 'Message text 2' } }} -
-
- {{ 'osd.dashboard.id-3' | i18n: { defaultMessage: 'Message text 3', description: 'Message description 3' } }} -
-
-`); - -const report = jest.fn(); - -describe('dev/i18n/extractors/html', () => { - beforeEach(() => { - report.mockClear(); - }); - - test('extracts default messages from HTML', () => { - const actual = Array.from(extractHtmlMessages(htmlSourceBuffer)); - expect(actual.sort()).toMatchSnapshot(); - }); - - test('extracts default messages from HTML with one-time binding', () => { - const actual = Array.from( - extractHtmlMessages(` -
- {{::'osd.id' | i18n: { defaultMessage: 'Message text with {value}', values: { value: 'value' } }}} -
-`) - ); - expect(actual.sort()).toMatchSnapshot(); - }); - - test('throws on empty i18n-id', () => { - const source = Buffer.from(`\ -

-`); - - expect(() => extractHtmlMessages(source, { report }).next()).not.toThrow(); - expect(report.mock.calls).toMatchSnapshot(); - }); - - test('throws on missing i18n-default-message attribute', () => { - const source = Buffer.from(`\ -

-`); - - expect(() => extractHtmlMessages(source, { report }).next()).not.toThrow(); - expect(report.mock.calls).toMatchSnapshot(); - }); - - test('throws on i18n filter usage in complex angular expression', () => { - const source = Buffer.from(`\ -
-`); - - expect(() => extractHtmlMessages(source, { report }).next()).not.toThrow(); - expect(report.mock.calls).toMatchSnapshot(); - }); - - test('extracts message from i18n filter in interpolating directive', () => { - const source = Buffer.from(` - -`); - - expect(Array.from(extractHtmlMessages(source))).toMatchSnapshot(); - }); -}); diff --git a/src/dev/i18n/extractors/index.js b/src/dev/i18n/extractors/index.js index 6e7601aa6599..bd501ba195b0 100644 --- a/src/dev/i18n/extractors/index.js +++ b/src/dev/i18n/extractors/index.js @@ -29,4 +29,3 @@ */ export { extractCodeMessages } from './code'; -export { extractHtmlMessages } from './html';