Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions dynamic-demo-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
"name": "@console/dynamic-demo-plugin",
"version": "0.0.0",
"private": true,
"resolutions": {
"@types/react": "17.0.39"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

# (cd frontend ; yarn why @types/react)
..
=> Found "@types/[email protected]"

For now, we should align all React related version resolutions with Console frontend.

Once we bump React to 18 along with any React related dependencies, we should try to eliminate this resolution override.

},
"scripts": {
"clean": "rm -rf dist",
"build": "yarn clean && NODE_ENV=production yarn ts-node node_modules/.bin/webpack",
Expand All @@ -20,7 +23,7 @@
"@patternfly/react-core": "^6.2.2",
"@patternfly/react-icons": "^6.2.2",
"@patternfly/react-table": "^6.2.2",
"@types/react": "16.8.13",
"@types/react": "17.0.39",
"@types/react-router": "^5.1.20",
"@types/react-router-dom": "5.3.x",
"copy-webpack-plugin": "^6.4.1",
Expand All @@ -29,9 +32,9 @@
"http-server": "0.12.x",
"i18next-parser": "^3.3.0",
"js-yaml": "^4.1.0",
"react": "17.0.1",
"react-dom": "17.0.1",
"react-i18next": "^11.7.3",
"react": "^17.0.1",
"react-dom": "^17.0.1",
Comment on lines +35 to +36
Copy link
Member

@logonoff logonoff Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why was this change in semver range necessary? I mean I will be updating this in #14869 anyway so it's okay

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this was done to keep React deps in sync with Console frontend package.json

For both Console frontend and dynamic demo plugin, I'd prefer that we use a specific React version instead of version range.

So let's revisit this once we bump React to 18 in Console frontend.

"react-i18next": "^15.1.4",
"react-router": "5.3.x",
"react-router-dom": "5.3.x",
"react-router-dom-v5-compat": "^6.11.2",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import * as React from 'react';
import {
K8sResourceCommon,
useK8sWatchResource,
Expand Down
2,511 changes: 1,575 additions & 936 deletions dynamic-demo-plugin/yarn.lock

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@
"graphql": "^14.0.0",
"history": "^4.9.0",
"hoist-non-react-statics": "3.x",
"i18next": "^21.10.0",
"i18next": "^23.16.8",
"i18next-browser-languagedetector": "^6.0.1",
"i18next-conv": "12.1.1",
"i18next-http-backend": "^1.0.21",
Expand All @@ -202,7 +202,7 @@
"react-dnd-html5-backend": "^11.1.3",
"react-dom": "^17.0.1",
"react-helmet-async": "^2.0.5",
"react-i18next": "^11.12.0",
"react-i18next": "^15.1.4",
"react-linkify": "^0.2.2",
"react-modal": "^3.16.3",
"react-redux": "7.2.9",
Expand Down
10 changes: 6 additions & 4 deletions frontend/packages/console-app/src/components/nodes/NodesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -269,10 +269,12 @@ const useNodesColumns = (): TableColumn<NodeRowItem>[] => {
const CPUCell: React.FC<{ cores: number; totalCores: number }> = ({ cores, totalCores }) => {
const { t } = useTranslation();
return Number.isFinite(cores) && Number.isFinite(totalCores) ? (
t('console-app~{{formattedCores}} cores / {{totalCores}} cores', {
formattedCores: formatCores(cores),
totalCores,
})
<>
{t('console-app~{{formattedCores}} cores / {{totalCores}} cores', {
formattedCores: formatCores(cores),
totalCores,
})}
</>
) : (
<>{DASH}</>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ describe('translateExtension', () => {
},
};

const t = jest.fn<string, [string]>((key) => `translated: ${key}`);
const mockFn = jest.fn((key) => `translated: ${key}`);
const t = mockFn;

expect(translateExtension(testExtension, t)).toEqual({
type: 'Foo/Bar',
Expand All @@ -59,8 +60,8 @@ describe('translateExtension', () => {
},
});

expect(t.mock.calls.length).toBe(5);
expect(t.mock.calls).toEqual([
expect(mockFn.mock.calls.length).toBe(5);
expect(mockFn.mock.calls).toEqual([
['%test~1%'],
['%test~3%'],
['%test~5%'],
Expand All @@ -71,9 +72,10 @@ describe('translateExtension', () => {

it('returns the same extension instance', () => {
const testExtension: Extension = { type: 'Foo/Bar', properties: {} };
const t = jest.fn<string, []>();
const mockFn = jest.fn((key) => key);
const t = mockFn;

expect(translateExtension(testExtension, t)).toBe(testExtension);
expect(t).not.toHaveBeenCalled();
expect(mockFn).not.toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@ export const translateExtensionDeep = <E extends Extension>(
* Recursively updates the extension's properties, replacing all translatable string values
* via the provided `t` function.
*/
export const translateExtension = <E extends Extension>(extension: E, t: TFunction): E => {
export const translateExtension = <E extends Extension>(
extension: E,
t: TFunction | ((key: string) => string),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems there are lots of places that work with TFunction | ((key: string) => string) union.

The TFunction comes from React i18next while (key: string) => string is mainly for test purposes.

Maybe we can add e.g. ConsoleTFunction type to console-types in order to cut down the repetition?

export type ConsoleTFunction = TFunction | ((key: string) => string);

): E => {
translateExtensionDeep(extension, isTranslatableString, (value, key, obj) => {
obj[key] = t(value);
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { useCallback } from 'react';
import { TFunction } from 'i18next';
import { Namespace, useTranslation, UseTranslationOptions } from 'react-i18next';
import { Namespace } from 'i18next';
import { useTranslation, UseTranslationOptions } from 'react-i18next';
import { isTranslatableString, getTranslationKey } from './extension-i18n';

/**
* Extends i18next `useTranslation` hook and overrides the `t` function.
*
* Translatable strings in Console application must use the `%key%` pattern.
*/
const useTranslationExt = (ns?: Namespace, options?: UseTranslationOptions) => {
const useTranslationExt = (ns?: Namespace, options?: UseTranslationOptions<undefined>) => {
const result = useTranslation(ns, options);
const { t } = result;
const cb: TFunction = useCallback(
const cb = useCallback(
(value: string) => (isTranslatableString(value) ? t(getTranslationKey(value)) : value),
[t],
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { TFunction } from 'i18next';
import { CatalogItem } from '@console/dynamic-plugin-sdk/src';
import { ConsoleSample } from '../../../../types';
import { normalizeConsoleSamples } from '../useConsoleSamples';
import { gitImportSample, containerImportSample } from './useConsoleSamples.data';

export const t: TFunction = (key: string) =>
export const t = (key: string): string =>
key.includes('~') ? key.substring(key.indexOf('~') + 1) : key;

describe('normalizeConsoleSamples', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ import {
useSamples,
} from '../../../utils/samples';

export const normalizeConsoleSamples = (activeNamespace: string, t: TFunction) => {
export const normalizeConsoleSamples = (
activeNamespace: string,
t: TFunction | ((key: string) => string),
) => {
const createLabel = t('devconsole~Create');

return (sample: ConsoleSample): CatalogItem<ConsoleSample> | null => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@ const {
} = submitUtils;

describe('Import Submit Utils', () => {
const t = jest.fn();
const mockFn = jest.fn();
const t = mockFn;

describe('createDeployment tests', () => {
beforeAll(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -778,7 +778,7 @@ export const createDevfileResources = async (
};

export const createOrUpdateResources = async (
t: TFunction,
t: TFunction | ((key: string) => string),
formData: GitImportFormData,
imageStream: K8sResourceKind,
createNewProject?: boolean,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { TFunction } from 'react-i18next';
import { TFunction } from 'i18next';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 where possible, TFunction type should be imported from i18next.

react-i18next might be re-exporting it but I think it's more correct to import it from i18next.

import * as yup from 'yup';
import { RESOURCE_NAME_REGEX } from './regex';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -430,9 +430,6 @@
"Managed by": "Managed by",
"Installing": "Installing",
"Valid subscriptions": "Valid subscriptions",
"Pending": "Pending",
"OK": "OK",
"Failed": "Failed",
"This operator includes a console plugin which provides a custom interface that can be included in the console. The console plugin will prompt for the console to be refreshed once it has been enabled. Make sure you trust this console plugin before enabling.": "This operator includes a console plugin which provides a custom interface that can be included in the console. The console plugin will prompt for the console to be refreshed once it has been enabled. Make sure you trust this console plugin before enabling.",
"Hide operator description": "Hide operator description",
"Show operator description": "Show operator description",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,12 +214,12 @@ const ManagedNamespaces: React.FC<ManagedNamespacesProps> = ({ obj }) => {

switch (managedNamespaces.length) {
case 0:
return t('olm~All Namespaces');
return <>{t('olm~All Namespaces')}</>;
case 1:
return managedNamespaces[0] ? (
<ResourceLink kind="Namespace" title={managedNamespaces[0]} name={managedNamespaces[0]} />
) : (
t('olm~All Namespaces')
<>{t('olm~All Namespaces')}</>
);
default:
return (
Expand Down
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doesn't this remove the i18n for these strings?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the intention behind this may be that we don't want to translate such k8s object status values.

cc @rhamilto @cajieh

Original file line number Diff line number Diff line change
Expand Up @@ -42,18 +42,18 @@ export const getCSVStatus = (
// TODO Get rid of let.
let status: ClusterServiceVersionStatus;
if (pendingPhases.includes(statusPhase)) {
status = i18n.t('olm~Pending');
status = ClusterServiceVersionStatus.Pending;
} else {
switch (statusPhase) {
case ClusterServiceVersionPhase.CSVPhaseSucceeded:
status = i18n.t('olm~OK');
status = ClusterServiceVersionStatus.OK;
break;
case ClusterServiceVersionPhase.CSVPhaseFailed:
status = i18n.t('olm~Failed');
status = ClusterServiceVersionStatus.Failed;
break;
default:
return {
status: i18n.t('olm~Unknown'),
status: ClusterServiceVersionStatus.Unknown,
title: statusPhase,
};
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { render, screen } from '@testing-library/react';
import { TFunction } from 'react-i18next';
import { TFunction } from 'i18next';
import { getTopologyShortcuts } from '../components/graph-view/TopologyShortcuts';
import { TopologyViewType } from '../topology-types';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,6 @@
"Prometheus query failed.": "Prometheus query failed.",
"Invalid credentials": "Invalid credentials",
"Failing {{reason}}": "Failing {{reason}}",
"Failed to load kubecontrollermanager": "Failed to load kubecontrollermanager",
"Failed to parse cloud provider config {{cm}}": "Failed to parse cloud provider config {{cm}}",
"The following content was expected to be defined in the configMap: {{ expectedValues }}": "The following content was expected to be defined in the configMap: {{ expectedValues }}",
"Failed to persist {{secret}}": "Failed to persist {{secret}}",
"Failed to patch kubecontrollermanager": "Failed to patch kubecontrollermanager",
"Failed to patch cloud provider config": "Failed to patch cloud provider config",
"Failed to add taint to nodes": "Failed to add taint to nodes",
"Failed to patch infrastructure spec": "Failed to patch infrastructure spec",
"Unexpected error": "Unexpected error",
"vCenter": "vCenter",
"Enter the network address of the vCenter server. It can either be a domain name or IP address": "Enter the network address of the vCenter server. It can either be a domain name or IP address",
"vCenter cluster": "vCenter cluster",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
PrometheusHealthHandler,
} from '@console/dynamic-plugin-sdk';
import { ConfigMap } from '../../resources';
import { getVSphereHealth } from '../getVSphereHealth';
import { getVSphereHealth, TranslationFunction } from '../getVSphereHealth';
import { VSphereConnectionModal } from '../VSphereConnectionModal';
import { VSphereOperatorStatuses } from '../VSphereOperatorStatuses';
import './VSphereStatus.css';
Expand Down Expand Up @@ -54,7 +54,8 @@ const VSphereStatus: React.FC<PrometheusHealthPopupProps> = ({ hide, responses,
};

export const healthHandler: PrometheusHealthHandler = (responses, t, additionalResource) => {
const health = getVSphereHealth(t || (() => ''), responses, additionalResource);
const fallbackT: TranslationFunction = (key: string) => key;
const health = getVSphereHealth(t || fallbackT, responses, additionalResource);
const { state } = health;

let message: string | undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
t_color_green_50 as okColor,
t_color_red_60 as errorColor,
} from '@patternfly/react-tokens';
import { TFunction } from 'i18next';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom-v5-compat';
import {
Expand All @@ -19,6 +18,7 @@ import {
import { ClusterOperator } from '@console/internal/module/k8s';
import { CONSOLE_PREFIX_CLUSTER_OPERATOR, getCondition } from '../resources';
import { K8sResourceConditionStatus } from '../resources/k8sResource';
import { TranslationFunction } from './getVSphereHealth';

let ohlCounter = 0;
const OperatorHealthLevel: { [key: string]: number } = {
Expand Down Expand Up @@ -47,7 +47,7 @@ const getWorstIconState = (states: OperatorHealthType[]): OperatorHealthType['ic
return worst.icon;
};

const useOperatorHealth = (t: TFunction, name: string): OperatorHealthType => {
const useOperatorHealth = (t: TranslationFunction, name: string): OperatorHealthType => {
const [operator, isLoaded, error] = useK8sWatchResource<ClusterOperator>({
groupVersionKind: { group: 'config.openshift.io', version: 'v1', kind: 'ClusterOperator' },
name,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { TFunction } from 'i18next';
import { toInteger } from 'lodash';
import {
HealthState,
Expand All @@ -9,13 +8,15 @@ import {
} from '@console/dynamic-plugin-sdk';
import { ConfigMap } from '../resources';

export type TranslationFunction = (key: string, options?: Record<string, unknown>) => string;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be aligned with proposed ConsoleTFunction so that we use a single i18n function type in our code?

Also not sure we need the options part which I suppose comes from TFunction typings (?)


Comment on lines +11 to +12
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n frontend/packages/vsphere-plugin/src/components/getVSphereHealth.ts

Repository: openshift/console

Length of output: 3712


🏁 Script executed:

rg -nP --type=ts --type=tsx -C3 'getVSphereHealth\s*\(' -g '!*.spec.ts' -g '!*.test.ts'

Repository: openshift/console

Length of output: 88


🏁 Script executed:

rg -nP --type=ts -C3 'getVSphereHealth\s*\(' -g '!*.spec.ts' -g '!*.test.ts'

Repository: openshift/console

Length of output: 1848


🏁 Script executed:

cat -n frontend/packages/vsphere-plugin/src/components/ClusterOverview/VSphereStatus.tsx

Repository: openshift/console

Length of output: 2823


Type abstraction is well-designed and consumers are compatible. The TranslationFunction type successfully abstracts the translation interface, improving testability as shown by the fallback implementation in healthHandler. All consumers pass compatible signatures.

However, fix missing translation wrapper at line 76: the message 'vsphere-plugin~Synchronization failed' should be wrapped with t() to match the pattern used elsewhere in the function and ensure proper translation:

message: t('vsphere-plugin~Synchronization failed')
🤖 Prompt for AI Agents
In frontend/packages/vsphere-plugin/src/components/getVSphereHealth.ts around
lines 11 and 76, the TranslationFunction type is fine but at line 76 the error
message string 'vsphere-plugin~Synchronization failed' is not wrapped with the
translation function; update that message to call
t('vsphere-plugin~Synchronization failed') so it follows the established pattern
and returns a translated string consistent with the rest of the function.

const getPrometheusMetricValue = (
prometheusResult: PrometheusResult[],
reason: string,
): PrometheusValue | undefined => prometheusResult.find((r) => r.metric.reason === reason)?.value;

export const getVSphereHealth = (
t: TFunction,
t: TranslationFunction,
responses: PrometheusHealthPopupProps['responses'],
configMapResult: PrometheusHealthPopupProps['k8sResult'],
): SubsystemHealth => {
Expand Down
14 changes: 7 additions & 7 deletions frontend/packages/vsphere-plugin/src/components/persist.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { safeLoad, dump } from 'js-yaml';
import { TFunction } from 'react-i18next';
import {
k8sCreate,
k8sGet,
Expand All @@ -15,6 +14,7 @@ import {
VSPHERE_CREDS_SECRET_NAMESPACE,
} from '../constants';
import { ConfigMap, Infrastructure, KubeControllerManager, Secret } from '../resources';
import { TranslationFunction } from './getVSphereHealth';
import { ConnectionFormFormikValues, PersistOp, ProviderCM } from './types';
import { encodeBase64, getErrorMessage } from './utils';

Expand Down Expand Up @@ -92,7 +92,7 @@ const getPersistSecretOp = async (
};

const getPatchKubeControllerManagerOp = async (
t: TFunction<'vsphere-plugin'>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that we can utilize the Ns generic type param in the proposed ConsoleTFunction type.

export type ConsoleTFunction<Ns extends Namespace = DefaultNamespace> = TFunction<Ns> | ((key: string) => string);

t: TranslationFunction,
kubeControllerManagerModel: K8sModel,
): Promise<PersistOp> => {
let cm: KubeControllerManager;
Expand Down Expand Up @@ -129,7 +129,7 @@ const getPatchKubeControllerManagerOp = async (
};

const updateYamlFormat = (
t: TFunction<'vsphere-plugin'>,
t: TranslationFunction,
values: ConnectionFormFormikValues,
initValues: ConnectionFormFormikValues,
cloudProviderConfig: ConfigMap,
Expand Down Expand Up @@ -249,7 +249,7 @@ const getUpdatedConfigMapResourcePool = (
};

const updateIniFormat = (
t: TFunction<'vsphere-plugin'>,
t: TranslationFunction,
values: ConnectionFormFormikValues,
initValues: ConnectionFormFormikValues,
cloudProviderConfig: ConfigMap,
Expand Down Expand Up @@ -360,7 +360,7 @@ const fixConfigMap = (values: ConnectionFormFormikValues) => {
};

const getPersistProviderConfigMapOp = async (
t: TFunction<'vsphere-plugin'>,
t: TranslationFunction,
configMapModel: K8sModel,
values: ConnectionFormFormikValues,
initValues: ConnectionFormFormikValues,
Expand Down Expand Up @@ -551,7 +551,7 @@ const runPatches = async ({
addTaints,
queryParams,
}: {
t: TFunction<'vsphere-plugin'>;
t: TranslationFunction;
persistSecret: PersistOp;
persistKubeControllerManager: PersistOp;
persistProviderCM: PersistOp;
Expand Down Expand Up @@ -599,7 +599,7 @@ const runPatches = async ({
};

export const persist = async (
t: TFunction<'vsphere-plugin'>,
t: TranslationFunction,
{
secretModel,
configMapModel,
Expand Down
Loading