diff --git a/.gitignore b/.gitignore index 42b73536ed5..f6687f3a0e6 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ install-state.gz !.yarn/releases !.yarn/sdks !.yarn/versions + !/vendor/** !.vscode/settings.json .DS_Store @@ -17,11 +18,9 @@ cypress-a11y-report.json /bin /gopath /Godeps/_workspace/src/github.com/openshift/console -/frontend/.cache-loader /frontend/.puppeteer /frontend/.webpack-cycles /frontend/__coverage__ -/frontend/__chrome_browser__ /frontend/**/node_modules /frontend/**/npm-debug.log /frontend/**/yarn-error.log diff --git a/Dockerfile.plugins.demo b/Dockerfile.plugins.demo index 50f48efacd4..b639eacfcd5 100644 --- a/Dockerfile.plugins.demo +++ b/Dockerfile.plugins.demo @@ -9,7 +9,7 @@ RUN mkdir -p /src/console COPY . /src/console WORKDIR /src/console/frontend -RUN yarn install && yarn build-plugin-sdk +RUN yarn install && yarn generate WORKDIR /src/console/dynamic-demo-plugin RUN yarn install && yarn build diff --git a/INTERNATIONALIZATION.md b/INTERNATIONALIZATION.md index a1934697766..b83244052da 100644 --- a/INTERNATIONALIZATION.md +++ b/INTERNATIONALIZATION.md @@ -19,7 +19,7 @@ cd frontend yarn i18n ``` -This command launches the [code parser](https://github.com/i18next/i18next-parser), generates JSON files containing English key:value pairs for all internationalized strings, and consolidates any English JSON files with identical names to avoid namespace conflicts in i18next. +This command launches the [code parser](https://github.com/i18next/i18next-cli), generates JSON files containing English key:value pairs for all internationalized strings, and consolidates any English JSON files with identical names to avoid namespace conflicts in i18next. #### Scope We are not able to translate all text in the application. Text located in backend code or non-Red-Hat-controlled development environments may not be accessible for translation. @@ -55,7 +55,7 @@ Good: t('public~Hello, it is now {{date}}', { date: new Date() }) ``` model.labelPluralKey ? t(model.labelPluralKey) : model.labelPlural ``` -* While i18next extracts translation keys in runtime, i18next-parser (the tool we use to generate JSON files) doesn't run the code, so it can't interpolate values in these expressions: +* At runtime, i18next resolves translation keys to translated text. At build time, i18next-cli (the tool we use to generate JSON files) extracts keys from source code via static analysis without executing it, so it cannot resolve dynamic expressions like: ``` t(key) diff --git a/clean-frontend.sh b/clean-frontend.sh index b23c086515f..6c5bcb760f1 100755 --- a/clean-frontend.sh +++ b/clean-frontend.sh @@ -1,5 +1,4 @@ #!/usr/bin/env bash find frontend -type d -name 'node_modules' -prune -exec rm -rf {} \; -rm -rf frontend/.cache-loader rm -rf frontend/public/dist diff --git a/dynamic-demo-plugin/i18next.config.ts b/dynamic-demo-plugin/i18next.config.ts index c0886f2cf16..40e7a994e63 100644 --- a/dynamic-demo-plugin/i18next.config.ts +++ b/dynamic-demo-plugin/i18next.config.ts @@ -1,47 +1,5 @@ -import { readFile } from 'node:fs/promises'; -import jsonc from 'comment-json'; -import { defineConfig, Plugin } from 'i18next-cli'; - -/** - * Custom JSON parser for localizing keys matching format: /%.+%/ - */ -const consoleExtensionsPlugin = (): Plugin => ({ - name: 'console-extensions', - - async onEnd(keys) { - const content = await readFile('console-extensions.json', 'utf-8'); - const extracted: { key: string }[] = []; - - try { - jsonc.parse( - content, - (_key, value) => { - if (typeof value === 'string') { - const match = value.match(/^%(.+)%$/); - if (match && match[1]) { - extracted.push({ key: match[1] }); - } - } - return value; - }, - true, - ); - } catch (e) { - console.error('Failed to parse as JSON.', e); - extracted.length = 0; - } - - for (const { key: fullKey } of extracted) { - const [ns, key] = fullKey.split('~', 2); - - if (ns && key) { - keys.set(`${ns}:${key}`, { key, defaultValue: key, ns }); - } else { - console.warn(`Invalid key format: ${fullKey}`); - } - } - }, -}); +import { ConsoleExtensionsI18nextCliPlugin } from '../frontend/i18n-scripts/ConsoleExtensionsI18nextCliPlugin'; +import { defineConfig } from 'i18next-cli'; export default defineConfig({ locales: ['en'], @@ -54,5 +12,5 @@ export default defineConfig({ nsSeparator: '~', defaultNS: 'plugin__console-demo-plugin', }, - plugins: [consoleExtensionsPlugin()], + plugins: [ConsoleExtensionsI18nextCliPlugin()], }); diff --git a/frontend/.prettierignore b/frontend/.prettierignore index d8a498412b1..b12d2e23211 100644 --- a/frontend/.prettierignore +++ b/frontend/.prettierignore @@ -1,4 +1,3 @@ -.cache-loader .yarn node_modules public/dist diff --git a/frontend/__tests__/i18n-structure.spec.ts b/frontend/__tests__/i18n-structure.spec.ts index fcd51ac060c..8198e4917ad 100644 --- a/frontend/__tests__/i18n-structure.spec.ts +++ b/frontend/__tests__/i18n-structure.spec.ts @@ -44,6 +44,42 @@ describe('i18n structure', () => { expect(mismatches).toEqual([]); }); + it('must have different values for _one and _other plural keys in EN', () => { + const enLocaleFiles = LOCALE_FILES.filter((file) => file.includes('/en/')); + const identical: string[] = []; + + for (const file of enLocaleFiles) { + const raw = readFileSync(resolve(FRONTEND_DIR, file), 'utf-8'); + const data = JSON.parse(raw); + const lines = raw.split('\n'); + + for (const [key, value] of Object.entries(data)) { + if (key.endsWith('_one')) { + const base = key.slice(0, -4); + const otherValue = data[`${base}_other`]; + if (otherValue !== undefined && value === otherValue) { + const line = lines.findIndex((l) => l.includes(`"${key}"`)) + 1; + identical.push(`${file}:${line}: "${base}"`); + } + } + } + } + + if (identical.length > 0) { + throw new Error( + [ + 'Found _one/_other pairs with identical values:', + '', + ...identical, + '', + 'Hint: If the singular and plural forms are genuinely identical, use a', + 'variable name other than "count" (e.g. "numberOf") to avoid i18next', + 'plural resolution and use a single key instead of _one/_other pairs.', + ].join('\n'), + ); + } + }); + it('must contain only one file per locale directory', () => { const dirCounts: Record = {}; for (const file of LOCALE_FILES) { diff --git a/frontend/i18n-scripts/ConsoleExtensionsI18nextCliPlugin.ts b/frontend/i18n-scripts/ConsoleExtensionsI18nextCliPlugin.ts new file mode 100644 index 00000000000..0160a250c2d --- /dev/null +++ b/frontend/i18n-scripts/ConsoleExtensionsI18nextCliPlugin.ts @@ -0,0 +1,77 @@ +/* eslint-disable no-console */ +import { readFile } from 'node:fs/promises'; +import chalk from 'chalk'; +import { parse } from 'comment-json'; +import type { Plugin } from 'i18next-cli'; + +export interface ConsoleExtensionsI18nextCliPluginOptions { + /** + * Paths to the console-extensions.json file, assumed to be encoded as UTF-8. + * + * Defaults to `console-extensions.json` in the current working directory. + */ + paths?: string[]; +} + +/** + * A [i18next-cli] plugin to extract translation keys from `console-extensions.json` files. + * + * Keys matching the format `%namespace~key%` are extracted. + * + * NOTE: `i18next-cli` does not appear to fully respect semver. Compatibility is only + * known if you are using the exact same version of `i18next-cli` that Console is using. + * + * @returns a [i18next-cli] plugin instance + * + * [i18next-cli]: https://github.com/i18next/i18next-cli + */ +export const ConsoleExtensionsI18nextCliPlugin = ({ + paths = ['console-extensions.json'], +}: ConsoleExtensionsI18nextCliPluginOptions = {}): Plugin => ({ + name: 'console-extensions', + + async onEnd(keys) { + const files = await Promise.all( + paths.map((path) => + readFile(path, 'utf-8').catch(() => { + console.warn(chalk.yellowBright(`Warning: Could not read file at ${path}. Skipping.`)); + return '{}'; + }), + ), + ); + + for (const [idx, content] of files.entries()) { + const extracted: { key: string }[] = []; + + try { + parse( + content, + (_key, value) => { + if (typeof value === 'string') { + const match = value.match(/^%(.+)%$/); + if (match && match[1]) { + extracted.push({ key: match[1] }); + } + } + return value; + }, + true, + ); + } catch (e) { + console.error(`Failed to parse ${paths[idx]}:`, e); + throw e; + } + + for (const { key: fullKey } of extracted) { + const sep = fullKey.indexOf('~'); + if (sep > 0 && sep < fullKey.length - 1) { + const ns = fullKey.slice(0, sep); + const key = fullKey.slice(sep + 1); + keys.set(`${ns}:${key}`, { key, defaultValue: key, ns }); + } else { + console.warn(chalk.yellowBright(`Invalid key format in ${paths[idx]}: ${fullKey}`)); + } + } + } + }, +}); diff --git a/frontend/i18n-scripts/__tests__/ConsoleExtensionsI18nextCliPlugin.spec.ts b/frontend/i18n-scripts/__tests__/ConsoleExtensionsI18nextCliPlugin.spec.ts new file mode 100644 index 00000000000..a0ed8fc1936 --- /dev/null +++ b/frontend/i18n-scripts/__tests__/ConsoleExtensionsI18nextCliPlugin.spec.ts @@ -0,0 +1,96 @@ +import { readFile } from 'node:fs/promises'; +import type { ExtractedKey } from 'i18next-cli'; +import { + ConsoleExtensionsI18nextCliPlugin, + type ConsoleExtensionsI18nextCliPluginOptions, +} from '../ConsoleExtensionsI18nextCliPlugin'; + +jest.mock('node:fs/promises', () => ({ + readFile: jest.fn(), +})); + +const mockedReadFile = readFile as unknown as jest.Mock; + +function getOnEnd(options?: ConsoleExtensionsI18nextCliPluginOptions) { + const { onEnd } = ConsoleExtensionsI18nextCliPlugin(options); + if (!onEnd) { + throw new Error('onEnd is not defined'); + } + return onEnd; +} + +describe('ConsoleExtensionsI18nextCliPlugin', () => { + let keys: Map; + + beforeEach(() => { + keys = new Map(); + mockedReadFile.mockReset(); + jest.spyOn(console, 'warn').mockImplementation(); + jest.spyOn(console, 'error').mockImplementation(); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should fail gracefully with invalid JSON', async () => { + mockedReadFile.mockResolvedValue('{"key": "%test~value%", invalid}'); + const onEnd = getOnEnd({ paths: ['test.json'] }); + await expect(onEnd(keys)).rejects.toThrow(); + }); + + it('should parse strings matching pattern `^%.+%$`', async () => { + mockedReadFile.mockResolvedValue( + '{"nope": false, "foo": "%ns~bar%", "test": ["%ns~arr1%", "%ns~arr2%", "arr3"]}', + ); + const onEnd = getOnEnd({ paths: ['test.json'] }); + await onEnd(keys); + expect([...keys.values()]).toEqual([ + { key: 'bar', defaultValue: 'bar', ns: 'ns' }, + { key: 'arr1', defaultValue: 'arr1', ns: 'ns' }, + { key: 'arr2', defaultValue: 'arr2', ns: 'ns' }, + ]); + }); + + it('should parse json with comments', async () => { + mockedReadFile.mockResolvedValue( + `{"nope": false, + // comment + "foo": "%ns~bar%", "test": ["%ns~arr1%", + // comment + "%ns~arr2%", "arr3"]}`, + ); + const onEnd = getOnEnd({ paths: ['test.json'] }); + await onEnd(keys); + expect([...keys.values()]).toEqual([ + { key: 'bar', defaultValue: 'bar', ns: 'ns' }, + { key: 'arr1', defaultValue: 'arr1', ns: 'ns' }, + { key: 'arr2', defaultValue: 'arr2', ns: 'ns' }, + ]); + }); + + it('should warn on keys without namespace separator', async () => { + mockedReadFile.mockResolvedValue('{"key": "%nonamespace%"}'); + const onEnd = getOnEnd({ paths: ['test.json'] }); + await onEnd(keys); + expect(keys.size).toBe(0); + expect(console.warn).toHaveBeenCalledWith(expect.stringContaining('Invalid key format')); + }); + + it('should treat everything after the first ~ as the key', async () => { + mockedReadFile.mockResolvedValue('{"key": "%ns~key~suffix%"}'); + const onEnd = getOnEnd({ paths: ['test.json'] }); + await onEnd(keys); + expect([...keys.values()]).toEqual([ + { key: 'key~suffix', defaultValue: 'key~suffix', ns: 'ns' }, + ]); + }); + + it('should warn and skip when file cannot be read', async () => { + mockedReadFile.mockRejectedValue(new Error('ENOENT')); + const onEnd = getOnEnd({ paths: ['missing.json'] }); + await onEnd(keys); + expect(keys.size).toBe(0); + expect(console.warn).toHaveBeenCalled(); + }); +}); diff --git a/frontend/i18next.config.ts b/frontend/i18next.config.ts index e2e2f0db14e..0fa3715f461 100644 --- a/frontend/i18next.config.ts +++ b/frontend/i18next.config.ts @@ -1,54 +1,9 @@ -/* eslint-disable no-console */ -import { readFile } from 'fs/promises'; import { existsSync } from 'fs'; import { join } from 'path'; import { sync as glob } from 'glob'; -import { parse } from 'comment-json'; -import { defineConfig, Plugin } from 'i18next-cli'; +import { defineConfig } from 'i18next-cli'; import { namespaceToDirName } from './i18n-scripts/namespace-map'; - -/** - * Plugin to extract translation keys from console-extensions.json files. - * Keys matching the format %namespace~key% are extracted. - */ -const consoleExtensionsPlugin = (): Plugin => ({ - name: 'console-extensions', - - async onEnd(keys) { - for (const filePath of glob('packages/*/console-extensions.json')) { - const content = await readFile(filePath, 'utf-8'); - const extracted: { key: string }[] = []; - - try { - parse( - content, - (_key, value) => { - if (typeof value === 'string') { - const match = value.match(/^%(.+)%$/); - if (match && match[1]) { - extracted.push({ key: match[1] }); - } - } - return value; - }, - true, - ); - } catch (e) { - console.error(`Failed to parse ${filePath}:`, e); - throw e; - } - - for (const { key: fullKey } of extracted) { - const [ns, key] = fullKey.split('~', 2); - if (ns && key) { - keys.set(`${ns}:${key}`, { key, defaultValue: key, ns }); - } else { - console.warn(`Invalid key format in ${filePath}: ${fullKey}`); - } - } - } - }, -}); +import { ConsoleExtensionsI18nextCliPlugin } from './i18n-scripts/ConsoleExtensionsI18nextCliPlugin'; export default defineConfig({ locales: ['en'], @@ -84,5 +39,9 @@ export default defineConfig({ lint: { ignore: ['**/*.spec.{js,jsx,ts,tsx}', '**/__tests__/**'], }, - plugins: [consoleExtensionsPlugin()], + plugins: [ + ConsoleExtensionsI18nextCliPlugin({ + paths: glob('packages/*/console-extensions.json'), + }), + ], }); diff --git a/frontend/jest.config.ts b/frontend/jest.config.ts index bf38dc4ca95..6c23e0c4e0a 100644 --- a/frontend/jest.config.ts +++ b/frontend/jest.config.ts @@ -29,8 +29,6 @@ export default defineConfig({ type: 'commonjs', noInterop: true, }, - // prettier too old to support satisfies operator - // eslint-disable-next-line prettier/prettier } satisfies SwcOptions, ], }, diff --git a/frontend/package.json b/frontend/package.json index b2ac0cbc176..90c9980be3f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,7 +16,7 @@ "clean": "rm -rf ./public/dist && yarn --cwd packages/console-dynamic-plugin-sdk clean", "dev": "yarn clean && yarn generate-plugin-sdk-schema && REACT_REFRESH=true NODE_OPTIONS=--max-old-space-size=4096 yarn ts-node ./node_modules/.bin/rspack serve --mode=development", "dev-once": "yarn clean && yarn generate-plugin-sdk-schema && NODE_OPTIONS=--max-old-space-size=4096 yarn ts-node ./node_modules/.bin/rspack --mode=development", - "build": "yarn clean && yarn build-plugin-sdk && NODE_ENV=production NODE_OPTIONS=--max-old-space-size=4096 yarn ts-node ./node_modules/.bin/rspack --mode=production", + "build": "yarn clean && yarn generate && NODE_ENV=production NODE_OPTIONS=--max-old-space-size=4096 yarn ts-node ./node_modules/.bin/rspack --mode=production", "check-cycles": "CHECK_CYCLES=true yarn dev-once", "coverage": "jest --coverage .", "eslint": "node ./node_modules/.bin/eslint --max-warnings 0 --color", @@ -49,9 +49,9 @@ "knip": "knip --config scripts/knip.ts", "prettier-all": "prettier --write '**/*.{js,jsx,ts,tsx,json}'", "ts-node": "ts-node -O '{\"module\":\"commonjs\"}'", + "generate": "yarn --cwd packages/console-dynamic-plugin-sdk build", "generate-plugin-sdk-docs": "yarn --cwd packages/console-dynamic-plugin-sdk generate-doc", "generate-plugin-sdk-schema": "yarn --cwd packages/console-dynamic-plugin-sdk generate-schema", - "build-plugin-sdk": "yarn --cwd packages/console-dynamic-plugin-sdk build", "i18n-to-po": "node ./i18n-scripts/i18n-to-po.js", "po-to-i18n": "node ./i18n-scripts/po-to-i18n.js", "i18n": "i18next-cli lint && i18next-cli extract", diff --git a/frontend/packages/console-app/locales/en/console-app.json b/frontend/packages/console-app/locales/en/console-app.json index 24d98e18354..1c5f0e0abcd 100644 --- a/frontend/packages/console-app/locales/en/console-app.json +++ b/frontend/packages/console-app/locales/en/console-app.json @@ -7,7 +7,7 @@ "{{count}} line_one": "{{count}} line", "{{count}} line_other": "{{count}} lines", "{{count}} PodDisruptionBudget violated_one": "{{count}} PodDisruptionBudget violated", - "{{count}} PodDisruptionBudget violated_other": "{{count}} PodDisruptionBudget violated", + "{{count}} PodDisruptionBudget violated_other": "{{count}} PodDisruptionBudgets violated", "{{duration, number}} minutes": "{{duration, number}} minutes", "{{enabledCount}}/{{totalCount}} enabled": "{{enabledCount}}/{{totalCount}} enabled", "{{fileName}} cannot be uploaded. Only {{fileExtensions}} files are supported currently. Try another file.": "{{fileName}} cannot be uploaded. Only {{fileExtensions}} files are supported currently. Try another file.", @@ -647,8 +647,7 @@ "Select a log file above": "Select a log file above", "Select a path": "Select a path", "Select account kind": "Select account kind", - "Select all <1>{{count}} {{label}}._one": "Select all {{count}} {{label}}.", - "Select all <1>{{count}} {{label}}._other": "Select all {{count}} {{label}}.", + "Select all <1>{{numberOf}} {{label}}.": "Select all <1>{{numberOf}} {{label}}.", "Select an option": "Select an option", "Select AWS Type": "Select AWS Type", "Select AWS Type. Default is gp3": "Select AWS Type. Default is gp3", @@ -821,11 +820,9 @@ "You cannot undo this action. Deleting a node signals to Kubernetes that the node is unrecoverable, which deletes all pods scheduled to it. If you delete a node that is still running but unresponsive, stateful workloads and persistent volumes might suffer data loss or corruption. Only delete a node after you confirm that it has completely stopped and you cannot restore it.": "You cannot undo this action. Deleting a node signals to Kubernetes that the node is unrecoverable, which deletes all pods scheduled to it. If you delete a node that is still running but unresponsive, stateful workloads and persistent volumes might suffer data loss or corruption. Only delete a node after you confirm that it has completely stopped and you cannot restore it.", "You do not have permission to edit groups. Contact your administrator for access.": "You do not have permission to edit groups. Contact your administrator for access.", "You do not have sufficient permissions to read any cluster configuration.": "You do not have sufficient permissions to read any cluster configuration.", - "You selected all {{count}} {{label}}._one": "You selected all {{count}} {{label}}.", - "You selected all {{count}} {{label}}._other": "You selected all {{count}} {{label}}.s", "You selected all {{label}} on this page.": "You selected all {{label}} on this page.", - "You selected all <1>{{count}} {{label}}._one": "You selected all {{count}} {{label}}.", - "You selected all <1>{{count}} {{label}}._other": "You selected all {{count}} {{label}}.", + "You selected all {{numberOf}} {{label}}.": "You selected all {{numberOf}} {{label}}.", + "You selected all <1>{{numberOf}} {{label}}.": "You selected all <1>{{numberOf}} {{label}}.", "You’re ready to go!": "You’re ready to go!", "Your progress will be saved.": "Your progress will be saved.", "Zone": "Zone", diff --git a/frontend/packages/console-app/src/components/data-view/ConsoleDataView.tsx b/frontend/packages/console-app/src/components/data-view/ConsoleDataView.tsx index 5178360a3d3..793ac3de5ec 100644 --- a/frontend/packages/console-app/src/components/data-view/ConsoleDataView.tsx +++ b/frontend/packages/console-app/src/components/data-view/ConsoleDataView.tsx @@ -314,8 +314,8 @@ export const ConsoleDataView = < className="pf-v6-u-mb-md" screenReaderText={ bannerState.allSelected - ? t('You selected all {{count}} {{label}}.', { - count: filteredData.length, + ? t('You selected all {{numberOf}} {{label}}.', { + numberOf: filteredData.length, label: label || t('items'), }) : t('You selected all {{label}} on this page.', { @@ -325,8 +325,8 @@ export const ConsoleDataView = < > {bannerState.allSelected ? ( <> - - You selected all {{ count: filteredData.length }}{' '} + + You selected all {{ numberOf: filteredData.length }}{' '} {{ label: label || t('items') }}. {' '} diff --git a/frontend/packages/console-app/src/components/quick-starts/utils/quick-start-context.tsx b/frontend/packages/console-app/src/components/quick-starts/utils/quick-start-context.tsx index 88c4a0b3c9d..b7284829401 100644 --- a/frontend/packages/console-app/src/components/quick-starts/utils/quick-start-context.tsx +++ b/frontend/packages/console-app/src/components/quick-starts/utils/quick-start-context.tsx @@ -165,9 +165,9 @@ export const useValuesForQuickStartContext = (): QuickStartContextValues => { const resourceBundle = i18n.getResourceBundle(language, 'console-app') ?? {}; const processedResourceBundle = getProcessedResourceBundle(resourceBundle, language); - // https://github.com/i18next/i18next-parser#caveats + // https://github.com/i18next/i18next-cli#comment-based-extraction // Need to reference the t() function here for all the keys used in the quickstarts library - // so that the i18n-parser can find them, and keep them in sync with the locale json file. + // so that the i18n-cli can find them, and keep them in sync with the locale json file. // Changes made in this comment block take effect after `yarn i18n` is run. // const resources = [ // t('console-app~Quick starts'), diff --git a/frontend/packages/operator-lifecycle-manager/locales/en/olm.json b/frontend/packages/operator-lifecycle-manager/locales/en/olm.json index cc5c4d7a820..e236209f5c8 100644 --- a/frontend/packages/operator-lifecycle-manager/locales/en/olm.json +++ b/frontend/packages/operator-lifecycle-manager/locales/en/olm.json @@ -8,7 +8,7 @@ " Once the Operator is installed the required custom resource will be available for creation.": " Once the Operator is installed the required custom resource will be available for creation.", " Ready for use.": " Ready for use.", "(default)": "(default)", - "{{count}} Namespaces_one": "{{count}} Namespaces", + "{{count}} Namespaces_one": "{{count}} Namespace", "{{count}} Namespaces_other": "{{count}} Namespaces", "{{initializationResourceKind}} required": "{{initializationResourceKind}} required", "{{item}} can't be installed": "{{item}} can't be installed", diff --git a/frontend/packages/topology/locales/en/topology.json b/frontend/packages/topology/locales/en/topology.json index cbe4d0af538..4e9ed922274 100644 --- a/frontend/packages/topology/locales/en/topology.json +++ b/frontend/packages/topology/locales/en/topology.json @@ -102,6 +102,7 @@ "Loading is taking longer than expected": "Loading is taking longer than expected", "Location:": "Location:", "Logs not available yet": "Logs not available yet", + "Memory": "Memory", "MiB": "MiB", "Monitoring alert": "Monitoring alert", "Move": "Move", diff --git a/frontend/packages/topology/src/components/list-view/cells/MemoryCell.tsx b/frontend/packages/topology/src/components/list-view/cells/MemoryCell.tsx index dbcde761e62..2836f9bed9b 100644 --- a/frontend/packages/topology/src/components/list-view/cells/MemoryCell.tsx +++ b/frontend/packages/topology/src/components/list-view/cells/MemoryCell.tsx @@ -20,7 +20,7 @@ const MemoryCellComponent = memo(({ memoryByPod, total return (
- + {formatBytesAsMiB(totalBytes)} diff --git a/frontend/public/components/control-plane-machine-set.tsx b/frontend/public/components/control-plane-machine-set.tsx index 29ca32fd23b..43382613092 100644 --- a/frontend/public/components/control-plane-machine-set.tsx +++ b/frontend/public/components/control-plane-machine-set.tsx @@ -75,7 +75,7 @@ const ControlPlaneMachineSetCounts: FC = ({ r {t('Current count')} - {t('{{replicas}} machines', { replicas, count: replicas })} + {t('{{replicas}} machine', { replicas, count: replicas })} diff --git a/frontend/public/components/impersonate-notifier.tsx b/frontend/public/components/impersonate-notifier.tsx index 804d971ab57..0ab6e919ee3 100644 --- a/frontend/public/components/impersonate-notifier.tsx +++ b/frontend/public/components/impersonate-notifier.tsx @@ -70,7 +70,7 @@ export const ImpersonateNotifier = connect( } > - {t('{{count}} more', { count: remainingCount })} + {t('{{remaining}} more', { remaining: remainingCount })} ) : null; diff --git a/frontend/public/locales/en/public.json b/frontend/public/locales/en/public.json index e19dcaa4a47..e9774213abb 100644 --- a/frontend/public/locales/en/public.json +++ b/frontend/public/locales/en/public.json @@ -38,8 +38,6 @@ "{{count}} line_other": "{{count}} lines", "{{count}} minute_one": "{{count}} minute", "{{count}} minute_other": "{{count}} minutes", - "{{count}} more_one": "{{count}} more", - "{{count}} more_other": "{{count}} more", "{{count}} resource reached quota_one": "{{count}} resource reached quota", "{{count}} resource reached quota_other": "{{count}} resource reached quotas", "{{count}} second_one": "{{count}} second", @@ -78,15 +76,14 @@ "{{numRemaining}} more": "{{numRemaining}} more", "{{readyReplicas}} machine_one": "{{readyReplicas}} machine", "{{readyReplicas}} machine_other": "{{readyReplicas}} machines", - "{{readyReplicas}} machines_one": "{{readyReplicas}} machines", + "{{readyReplicas}} machines_one": "{{readyReplicas}} machine", "{{readyReplicas}} machines_other": "{{readyReplicas}} machines", "{{readyReplicas}} of {{count}} machine_one": "{{readyReplicas}} of {{count}} machine", "{{readyReplicas}} of {{count}} machine_other": "{{readyReplicas}} of {{count}} machines", "{{receiverTypeLabel}}": "{{receiverTypeLabel}}", + "{{remaining}} more": "{{remaining}} more", "{{replicas}} machine_one": "{{replicas}} machine", "{{replicas}} machine_other": "{{replicas}} machines", - "{{replicas}} machines_one": "{{replicas}} machines", - "{{replicas}} machines_other": "{{replicas}} machines", "{{resource}} updates are paused.": "{{resource}} updates are paused.", "{{resourceKind}} maintain the proper number of healthy machines.": "{{resourceKind}} maintain the proper number of healthy machines.", "{{resourceKinds}} create one or more pods and ensure that a specified number of them successfully terminate. When the specified number of completions is successfully reached, the job is complete.": "{{resourceKinds}} create one or more pods and ensure that a specified number of them successfully terminate. When the specified number of completions is successfully reached, the job is complete.", @@ -96,11 +93,11 @@ "{{titleVerb}} {{receiverTypeLabel}} {{defaultString}} Receiver": "{{titleVerb}} {{receiverTypeLabel}} {{defaultString}} Receiver", "{{titleVerb}} Receiver": "{{titleVerb}} Receiver", "{{type}} {{name}}": "{{type}} {{name}}", - "{{unavailableReplicas}} machines_one": "{{unavailableReplicas}} machines", + "{{unavailableReplicas}} machines_one": "{{unavailableReplicas}} machine", "{{unavailableReplicas}} machines_other": "{{unavailableReplicas}} machines", "{{updatedMCPNodes}} of {{totalMCPNodes}}": "{{updatedMCPNodes}} of {{totalMCPNodes}}", "{{updatedOperatorsCount}} of {{totalOperatorsCount}}": "{{updatedOperatorsCount}} of {{totalOperatorsCount}}", - "{{updatedReplicas}} machines_one": "{{updatedReplicas}} machines", + "{{updatedReplicas}} machines_one": "{{updatedReplicas}} machine", "{{updatedReplicas}} machines_other": "{{updatedReplicas}} machines", "{{value}} at {{date}}": "{{value}} at {{date}}", "{{x}}: {{y}}%": "{{x}}: {{y}}%", @@ -927,7 +924,7 @@ "MachineHealthCheck details": "MachineHealthCheck details", "MachineHealthChecks": "MachineHealthChecks", "Machines": "Machines", - "machines_one": "machines", + "machines_one": "machine", "machines_other": "machines", "MachineSet": "MachineSet", "MachineSet details": "MachineSet details", @@ -950,7 +947,7 @@ "Max unavailable": "Max unavailable", "Max unavailable machines": "Max unavailable machines", "Max unhealthy": "Max unhealthy", - "Maximum {{count}} detached sessions. Close an existing session to detach a new one._one": "Maximum {{count}} detached sessions. Close an existing session to detach a new one.", + "Maximum {{count}} detached sessions. Close an existing session to detach a new one._one": "Maximum {{count}} detached session. Close an existing session to detach a new one.", "Maximum {{count}} detached sessions. Close an existing session to detach a new one._other": "Maximum {{count}} detached sessions. Close an existing session to detach a new one.", "Maximum replicas:": "Maximum replicas:", "Me": "Me", diff --git a/test-frontend.sh b/test-frontend.sh index 33546b28ba1..252c32a4328 100755 --- a/test-frontend.sh +++ b/test-frontend.sh @@ -11,7 +11,7 @@ pushd frontend # Dynamic plugin SDK docs are generated as part of the build, check for changes GIT_STATUS="$(git status --short --untracked-files -- packages/console-dynamic-plugin-sdk/docs)" if [ -n "$GIT_STATUS" ]; then - echo "dynamic plugin sdk docs are not up to date. Run 'yarn build-plugin-sdk' then commit changes." + echo "dynamic plugin sdk docs are not up to date. Run 'yarn generate' then commit changes." git --no-pager diff exit 1 fi @@ -31,12 +31,6 @@ if ! yarn dedupe --strategy highest --check ; then exit 1 fi -if ! yarn run check-cycles; then - echo "Cycle(s) detected!" - cat .webpack-cycles - exit 1 -fi - yarn run knip yarn run gherkin-lint @@ -48,3 +42,10 @@ if [ "$OPENSHIFT_CI" = true ]; then else yarn run test fi + +# check-cycles cleans the SDK dist/production build which is needed for some unit tests +if ! yarn run check-cycles; then + echo "Cycle(s) detected!" + cat .webpack-cycles + exit 1 +fi