Skip to content

Commit

Permalink
Merge pull request #11430 from openshift-cherrypick-robot/cherry-pick…
Browse files Browse the repository at this point in the history
…-11348-to-release-4.9

[release-4.9] Bug 2081389: Translate Extensions On Each Language Change
  • Loading branch information
openshift-merge-robot committed May 26, 2022
2 parents c28f128 + ddd3dd3 commit 4b9d05d
Show file tree
Hide file tree
Showing 5 changed files with 97 additions and 22 deletions.
Expand Up @@ -24,10 +24,15 @@ describe('deepForOwn', () => {
deepForOwn<TestValue>(obj, testPredicate, valueCallback);

expect(valueCallback.mock.calls.length).toBe(5);
expect(valueCallback.mock.calls[0]).toEqual([{ test: 1 }, 'foo', obj]);
expect(valueCallback.mock.calls[1]).toEqual([{ test: 2 }, '0', obj.bar]);
expect(valueCallback.mock.calls[2]).toEqual([{ test: 3 }, '1', obj.bar]);
expect(valueCallback.mock.calls[3]).toEqual([{ test: 4 }, 'qux', obj.baz]);
expect(valueCallback.mock.calls[4]).toEqual([{ test: 6 }, '0', obj.baz.mux.boom]);
expect(valueCallback.mock.calls[0]).toEqual([{ test: 1 }, 'foo', obj, 'foo']);
expect(valueCallback.mock.calls[1]).toEqual([{ test: 2 }, '0', obj.bar, 'bar.0']);
expect(valueCallback.mock.calls[2]).toEqual([{ test: 3 }, '1', obj.bar, 'bar.1']);
expect(valueCallback.mock.calls[3]).toEqual([{ test: 4 }, 'qux', obj.baz, 'baz.qux']);
expect(valueCallback.mock.calls[4]).toEqual([
{ test: 6 },
'0',
obj.baz.mux.boom,
'baz.mux.boom.0',
]);
});
});
26 changes: 17 additions & 9 deletions frontend/packages/console-dynamic-plugin-sdk/src/utils/object.ts
Expand Up @@ -2,29 +2,37 @@ import * as _ from 'lodash';

const isPlainNonReactObject = (obj: any) => _.isPlainObject(obj) && !obj.$$typeof;

export type ValueCallback<T> = (value: T, key: string, container: {}, path: string) => void;
export type PredicateCheck<T> = (value: unknown, path: string) => value is T;

/**
* Recursive equivalent of `_.forOwn` function that traverses plain objects and arrays.
*/
export const deepForOwn = <T = any>(
obj: {},
predicate: (value: any) => value is T,
valueCallback: (value: T, key: string, container: {}) => void,
predicate: PredicateCheck<T>,
valueCallback: ValueCallback<T>,
pathParts: string[] = [],
) => {
const visitValue = (value: any, key: string, container: {}) => {
if (predicate(value)) {
valueCallback(value, key, container);
const visitValue = (value: any, key: string, container: {}, newPathParts: string[]) => {
const path = newPathParts.join('.');
if (predicate(value, path)) {
valueCallback(value, key, container, path);
} else if (isPlainNonReactObject(value)) {
deepForOwn(value, predicate, valueCallback);
deepForOwn(value, predicate, valueCallback, newPathParts);
}
};

_.forOwn<any>(obj, (value, key, container) => {
Object.keys(obj).forEach((key) => {
const value = obj[key];
const newPathParts = [...pathParts, key];
if (Array.isArray(value)) {
value.forEach((arrayElement, index) => {
visitValue(arrayElement, index.toString(), value);
const indexKey = index.toString();
visitValue(arrayElement, indexKey, value, [...newPathParts, indexKey]);
});
} else {
visitValue(value, key, container);
visitValue(value, key, obj, newPathParts);
}
});
};
10 changes: 4 additions & 6 deletions frontend/packages/console-plugin-sdk/src/api/useExtensions.ts
Expand Up @@ -2,8 +2,7 @@ import * as React from 'react';
import * as _ from 'lodash';
import { useForceRender } from '@console/shared/src/hooks/useForceRender';
import { Extension, ExtensionTypeGuard, LoadedExtension } from '../typings';
import { translateExtension } from '../utils/extension-i18n';
import useTranslationExt from '../utils/useTranslationExt';
import useTranslatedExtensions from '../utils/useTranslatedExtensions';
import { subscribeToExtensions } from './pluginSubscriptionService';

/**
Expand Down Expand Up @@ -51,16 +50,15 @@ export const useExtensions = <E extends Extension>(
const unsubscribeRef = React.useRef<VoidFunction>(null);
const extensionsInUseRef = React.useRef<LoadedExtension<E>[]>([]);
const latestTypeGuardsRef = React.useRef<ExtensionTypeGuard<E>[]>(typeGuards);
const { t } = useTranslationExt();

const trySubscribe = React.useCallback(() => {
if (unsubscribeRef.current === null) {
unsubscribeRef.current = subscribeToExtensions<E>((extensions) => {
extensionsInUseRef.current = extensions.map((e) => translateExtension(e, t));
extensionsInUseRef.current = extensions;
isMountedRef.current && forceRender();
}, ...latestTypeGuardsRef.current);
}
}, [forceRender, t]);
}, [forceRender]);

const tryUnsubscribe = React.useCallback(() => {
if (unsubscribeRef.current !== null) {
Expand All @@ -84,5 +82,5 @@ export const useExtensions = <E extends Extension>(
[tryUnsubscribe],
);

return extensionsInUseRef.current;
return useTranslatedExtensions<E>(extensionsInUseRef.current);
};
16 changes: 14 additions & 2 deletions frontend/packages/console-plugin-sdk/src/utils/extension-i18n.ts
@@ -1,5 +1,9 @@
import { TFunction } from 'i18next';
import { deepForOwn } from '@console/dynamic-plugin-sdk/src/utils/object';
import {
deepForOwn,
PredicateCheck,
ValueCallback,
} from '@console/dynamic-plugin-sdk/src/utils/object';
import { Extension } from '../typings';

export const isTranslatableString = (value): value is string => {
Expand All @@ -11,12 +15,20 @@ export const isTranslatableString = (value): value is string => {
export const getTranslationKey = (value: string) =>
isTranslatableString(value) ? value.substr(1, value.length - 2) : undefined;

export const translateExtensionDeep = <E extends Extension>(
extension: E,
translationStringPredicate: PredicateCheck<string>,
cb: ValueCallback<string>,
): void => {
deepForOwn(extension.properties, translationStringPredicate, cb);
};

/**
* 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 => {
deepForOwn(extension.properties, isTranslatableString, (value, key, obj) => {
translateExtensionDeep(extension, isTranslatableString, (value, key, obj) => {
obj[key] = t(value);
});

Expand Down
@@ -0,0 +1,52 @@
import * as React from 'react';
import { Extension, LoadedExtension } from '@console/dynamic-plugin-sdk/src/types';
import { isTranslatableString, translateExtensionDeep } from './extension-i18n';
import useTranslationExt from './useTranslationExt';

/**
* `translateExtensionDeep` mutates the extension for translations. We need to store a
* semi-permanent mapping of the translation keys values.
*
* Structured as: { [extension.UID]: { [propertyPathToTranslation]: translationKey } }
*/
const translationKeyMap: Record<string, Record<string, string>> = {};

const useTranslatedExtensions = <E extends Extension>(
extensions: LoadedExtension<E>[],
): typeof extensions => {
const { t } = useTranslationExt();

React.useMemo(
// Mutate "extensions" parameter only if changed (i.e. a flag-enabled or translations changed)
() =>
extensions.forEach((e) => {
const UID = e.uid;
translateExtensionDeep(
e,
(value, path): value is string => {
let translatableString = value;
if (translationKeyMap[UID]?.[path]) {
translatableString = translationKeyMap[UID][path];
}
return isTranslatableString(translatableString);
},
(value, key, obj, path) => {
if (!translationKeyMap[UID]) {
translationKeyMap[UID] = {};
}
if (!translationKeyMap[UID][path]) {
translationKeyMap[UID][path] = value;
}
// TODO: Fix mutation of extension - mirrors work done in translateExtension
// @see translateExtension()
obj[key] = t(translationKeyMap[UID][path]);
},
);
}),
[t, extensions],
);

return extensions;
};

export default useTranslatedExtensions;

0 comments on commit 4b9d05d

Please sign in to comment.