Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bug 1998347: fix user preference for language and sync with local storage #9902

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 2 additions & 3 deletions frontend/packages/ceph-storage-plugin/src/utils/common.tsx
@@ -1,12 +1,11 @@
import { TFunction } from 'i18next';
import * as React from 'react';

import { getLastLanguage } from '@console/app/src/components/user-preferences/language/getLastLanguage';
import { humanizePercentage } from '@console/internal/components/utils';

import { StorageClusterKind } from '../types';

const getLocale = () => localStorage.getItem('bridge/language');

export const checkArbiterCluster = (storageCluster: StorageClusterKind): boolean =>
storageCluster?.spec?.arbiter?.enable;

Expand All @@ -23,7 +22,7 @@ export const toList = (text: string[]): React.ReactNode => text.map((s) => <li k
export const calcPercentage = (value: number, total: number) =>
humanizePercentage((value * 100) / total).string;

export const twelveHoursdateTimeNoYear = new Intl.DateTimeFormat(getLocale() || undefined, {
export const twelveHoursdateTimeNoYear = new Intl.DateTimeFormat(getLastLanguage() || undefined, {
month: 'short',
day: 'numeric',
hour: 'numeric',
Expand Down
2 changes: 1 addition & 1 deletion frontend/packages/console-app/console-extensions.json
Expand Up @@ -164,7 +164,7 @@
"type": "console.user-preference/group",
"properties": {
"id": "language",
"label": "%console-app~Language & region%"
"label": "%console-app~Language%"
}
},
{
Expand Down
4 changes: 2 additions & 2 deletions frontend/packages/console-app/locales/en/console-app.json
Expand Up @@ -2,7 +2,7 @@
"Administrator": "Administrator",
"VolumeSnapshotContents": "VolumeSnapshotContents",
"General": "General",
"Language & region": "Language & region",
"Language": "Language",
"Notifications": "Notifications",
"Perspective": "Perspective",
"If a perspective is not selected, the console defaults to the last viewed.": "If a perspective is not selected, the console defaults to the last viewed.",
Expand All @@ -13,7 +13,6 @@
"Last viewed": "Last viewed",
"Form": "Form",
"YAML": "YAML",
"Language": "Language",
"Select the language you want to use for the console.": "Select the language you want to use for the console.",
"Delete {{kind}}": "Delete {{kind}}",
"Edit {{kind}}": "Edit {{kind}}",
Expand Down Expand Up @@ -226,6 +225,7 @@
"Guided tour": "Guided tour",
"Step {{stepNumber, number}}/{{totalSteps, number}}": "Step {{stepNumber, number}}/{{totalSteps, number}}",
"guided tour {{step, number}}": "guided tour {{step, number}}",
"Use the default browser language setting.": "Use the default browser language setting.",
"Select a language": "Select a language",
"Search project": "Search project",
"Search namespace": "Search namespace",
Expand Down
@@ -0,0 +1,10 @@
import * as React from 'react';
import { usePreferredLanguage, useLanguage } from '../user-preferences/language';

const DetectLanguage: React.MemoExoticComponent<() => any> = React.memo(() => {
const [preferredLanguage, , preferredLanguageLoaded] = usePreferredLanguage();
Copy link
Member

Choose a reason for hiding this comment

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

Did we lose the query parameter detection for language?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think i18next-browser-languageDetector handles that. The old language preference modal didn't seem to detect language from query params as well.
The Detect Language just populates the local storage on first load if there is a preferred language in configmap but the same does not reflect in local storage.
This will be useful if the user changes browser after setting a language preference.

useLanguage(preferredLanguage, preferredLanguageLoaded);
return null;
});

export default DetectLanguage;
Expand Up @@ -18,6 +18,7 @@ import {
} from '@console/shared';
import { useTelemetry } from '@console/shared/src/hooks/useTelemetry';
import { useUserSettings } from '@console/shared/src/hooks/useUserSettings';
import { getLastLanguage } from '../../user-preferences/language/getLastLanguage';

export { QuickStartContext };
export { QuickStartContextProvider };
Expand All @@ -26,7 +27,7 @@ export const getProcessedResourceBundle = (resourceBundle, lng) => {
const params = new URLSearchParams(window.location.search);
const pseudolocalizationEnabled = params.get('pseudolocalization') === 'true';

const language = lng || localStorage.getItem('bridge/language') || 'en';
const language = lng || getLastLanguage() || 'en';
let consoleBundle = resourceBundle;
if (pseudolocalizationEnabled && language === 'en') {
consoleBundle = {};
Expand Down Expand Up @@ -174,7 +175,7 @@ export const useValuesForQuickStartContext = (): QuickStartContextValues => {
[activeQuickStartID, setAllQuickStartStates, fireTelemetryEvent],
);

const language = localStorage.getItem('bridge/language') || 'en';
const language = getLastLanguage() || 'en';
Copy link
Member

Choose a reason for hiding this comment

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

Not new, but en is not the right default. Can you open a separate bug to look at this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

const resourceBundle = i18n.getResourceBundle(language, 'console-app');
const processedResourceBundle = getProcessedResourceBundle(resourceBundle, language);

Expand Down
@@ -0,0 +1,3 @@
.co-language-dropdown__system-default-checkbox {
margin-bottom: var(--pf-global--spacer--sm) !important;
}
@@ -1,17 +1,16 @@
import * as React from 'react';
import { QuickStartContext, QuickStartContextValues } from '@patternfly/quickstarts';
import { Skeleton, SelectOption, Select, SelectVariant } from '@patternfly/react-core';
import { Skeleton, SelectOption, Select, SelectVariant, Checkbox } from '@patternfly/react-core';
import { useTranslation } from 'react-i18next';
import { getProcessedResourceBundle } from '../../quick-starts/utils/quick-start-context';
import { supportedLocales } from './const';
import { useLanguage } from './useLanguage';
import { usePreferredLanguage } from './usePreferredLanguage';

import './LanguageDropdown.scss';

const LanguageDropdown: React.FC = () => {
const { i18n, t } = useTranslation();
const { setResourceBundle } = React.useContext<QuickStartContextValues>(QuickStartContext);
const { t } = useTranslation();
const [preferredLanguage, setPreferredLanguage, preferredLanguageLoaded] = usePreferredLanguage();
const [dropdownOpen, setDropdownOpen] = React.useState(false);

const selectOptions: JSX.Element[] = React.useMemo(
() =>
Object.keys(supportedLocales).map((lang) => (
Expand All @@ -22,54 +21,76 @@ const LanguageDropdown: React.FC = () => {
[],
);

const selectedLanguage =
preferredLanguage ||
// handles languages we support, languages we don't support, and subsets of languages we support (such as en-us, zh-cn, etc.)
i18n.languages.find((lang) => supportedLocales[lang]);
const [isUsingDefault, setIsUsingDefault] = React.useState<boolean>(!preferredLanguage);
const checkboxLabel: string = t('console-app~Use the default browser language setting.');

const onToggle = (isOpen: boolean) => setDropdownOpen(isOpen);
const onSelect = (_, selection: string) => {
if (selection !== preferredLanguage) {
i18n.changeLanguage(selection);
setPreferredLanguage(selection);
}
setDropdownOpen(false);
};

React.useEffect(() => {
const onLanguageChange = (lng: string) => {
// Update language resource of quick starts components
const resourceBundle = i18n.getResourceBundle(lng, 'console-app');
const processedBundle = getProcessedResourceBundle(resourceBundle, lng);
setResourceBundle(processedBundle, lng);
};
i18n.on('languageChanged', onLanguageChange);
const onUsingDefault = (checked: boolean) => {
setIsUsingDefault(checked);
if (checked) {
setPreferredLanguage(null);
}
};

useLanguage(preferredLanguage, preferredLanguageLoaded); // sync the preferred language with local storage and set the console language

return () => {
i18n.off('languageChanged', onLanguageChange);
};
});
React.useEffect(() => {
if (preferredLanguageLoaded) {
setIsUsingDefault(!preferredLanguage);
}
// run this hook only after resources have loaded
// to set the using default language checkbox when the form loads
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [preferredLanguageLoaded]);

return preferredLanguageLoaded ? (
<Select
variant={SelectVariant.single}
isOpen={dropdownOpen}
selections={selectedLanguage}
toggleId={'console.preferredLanguage'}
onToggle={onToggle}
onSelect={onSelect}
placeholderText={t('console-app~Select a language')}
data-test={'dropdown console.preferredLanguage'}
maxHeight={300}
>
{selectOptions}
</Select>
<>
<Checkbox
id="default-language-checkbox"
label={checkboxLabel}
isChecked={isUsingDefault}
onChange={onUsingDefault}
aria-label={checkboxLabel}
data-test="checkbox console.preferredLanguage"
className="co-language-dropdown__system-default-checkbox"
/>
<Select
variant={SelectVariant.single}
isOpen={dropdownOpen}
selections={preferredLanguage}
toggleId={'console.preferredLanguage'}
onToggle={onToggle}
onSelect={onSelect}
placeholderText={t('console-app~Select a language')}
aria-label={t('console-app~Select a language')}
data-test="dropdown console.preferredLanguage"
maxHeight={300}
isDisabled={isUsingDefault}
>
{selectOptions}
</Select>
</>
) : (
<Skeleton
height="30px"
width="100%"
data-test={'dropdown skeleton console.preferredLanguage'}
/>
<>
<Skeleton
height="15px"
width="100%"
data-test="checkbox skeleton console.preferredLanguage"
className="co-language-dropdown__system-default-checkbox"
/>
<Skeleton
height="30px"
width="100%"
data-test="dropdown skeleton console.preferredLanguage"
/>
</>
);
};

Expand Down
@@ -1,6 +1,7 @@
import * as React from 'react';
import { Select } from '@patternfly/react-core';
import { Checkbox, Select } from '@patternfly/react-core';
import { shallow, ShallowWrapper } from 'enzyme';
import { getLastLanguage } from '../getLastLanguage';
import LanguageDropdown from '../LanguageDropdown';
import { usePreferredLanguage } from '../usePreferredLanguage';

Expand All @@ -19,25 +20,25 @@ jest.mock('react-i18next', () => {
useTranslation: () => ({
t: (key: string) => key,
i18n: {
changeLanguage: jest.fn(),
getResourceBundle: jest.fn(),
on: jest.fn(),
off: jest.fn(),
languages: ['en'],
changeLanguage: jest.fn(),
},
}),
};
});

jest.mock('../../../quick-starts/utils/quick-start-context', () => ({
getProcessedResourceBundle: jest.fn(),
}));

jest.mock('../usePreferredLanguage', () => ({
usePreferredLanguage: jest.fn(),
}));

jest.mock('../getLastLanguage', () => ({
getLastLanguage: jest.fn(),
}));

const usePreferredLanguageMock = usePreferredLanguage as jest.Mock;
const getLastLanguageMock = getLastLanguage as jest.Mock;
const preferredLanguageValue = 'ja';

describe('LanguageDropdown', () => {
Expand All @@ -49,26 +50,43 @@ describe('LanguageDropdown', () => {

it('should render skeleton if user preferences have not loaded', () => {
usePreferredLanguageMock.mockReturnValue(['', jest.fn(), false]);
getLastLanguageMock.mockReturnValue(['']);
spyOn(React, 'useContext').and.returnValue({ getProcessedResourceBundle: jest.fn() });
wrapper = shallow(<LanguageDropdown />);
expect(
wrapper.find('[data-test="dropdown skeleton console.preferredLanguage"]').exists(),
).toBeTruthy();
});

it('should render select with value corresponding to preferred language if user preferences have loaded and preferred language is defined', () => {
it('should render checkbox in checked state and select in disabled state if user preferences have loaded and preferred language is not defined', () => {
usePreferredLanguageMock.mockReturnValue([undefined, jest.fn(), true]);
getLastLanguageMock.mockReturnValue(['']);
spyOn(React, 'useContext').and.returnValue({ getProcessedResourceBundle: jest.fn() });
wrapper = shallow(<LanguageDropdown />);
expect(wrapper.find('[data-test="checkbox console.preferredLanguage"]').exists()).toBeTruthy();
expect(wrapper.find(Checkbox).props().isChecked).toBe(true);
expect(wrapper.find('[data-test="dropdown console.preferredLanguage"]').exists()).toBeTruthy();
expect(wrapper.find(Select).props().isDisabled).toBe(true);
});

it('should render checkbox in unchecked state and select in enabled state if user preferences have loaded and preferred language is defined', () => {
usePreferredLanguageMock.mockReturnValue([preferredLanguageValue, jest.fn(), true]);
getLastLanguageMock.mockReturnValue(['']);
spyOn(React, 'useContext').and.returnValue({ getProcessedResourceBundle: jest.fn() });
wrapper = shallow(<LanguageDropdown />);
expect(wrapper.find('[data-test="checkbox console.preferredLanguage"]').exists()).toBeTruthy();
expect(wrapper.find(Checkbox).props().isChecked).toBe(false);
expect(wrapper.find('[data-test="dropdown console.preferredLanguage"]').exists()).toBeTruthy();
expect(wrapper.find(Select).props().selections).toEqual(preferredLanguageValue);
expect(wrapper.find(Select).props().isDisabled).toBe(false);
});

it('should render select with value from i18next languages if user preferences have loaded but preferred language is not defined', () => {
usePreferredLanguageMock.mockReturnValue([undefined, jest.fn(), true]);
it('should render select with value corresponding to preferred language if user preferences have loaded and preferred language is defined', () => {
usePreferredLanguageMock.mockReturnValue([preferredLanguageValue, jest.fn(), true]);
getLastLanguageMock.mockReturnValue(['']);
spyOn(React, 'useContext').and.returnValue({ getProcessedResourceBundle: jest.fn() });
wrapper = shallow(<LanguageDropdown />);
expect(wrapper.find('[data-test="checkbox console.preferredLanguage"]').exists()).toBeTruthy();
expect(wrapper.find('[data-test="dropdown console.preferredLanguage"]').exists()).toBeTruthy();
expect(wrapper.find(Select).props().selections).toEqual('en');
expect(wrapper.find(Select).props().selections).toEqual(preferredLanguageValue);
});
});
Expand Up @@ -4,3 +4,5 @@ export const supportedLocales = {
ko: '한국어',
ja: '日本語',
};

export const LAST_LANGUAGE_LOCAL_STORAGE_KEY = 'bridge/last-language';
@@ -0,0 +1,3 @@
import { LAST_LANGUAGE_LOCAL_STORAGE_KEY } from './const';

export const getLastLanguage = (): string => localStorage.getItem(LAST_LANGUAGE_LOCAL_STORAGE_KEY);
@@ -1 +1,3 @@
export { default as LanguageDropdown } from './LanguageDropdown';
export * from './usePreferredLanguage';
export * from './useLanguage';
@@ -0,0 +1,38 @@
import * as React from 'react';
import { QuickStartContext, QuickStartContextValues } from '@patternfly/quickstarts';
import { useTranslation } from 'react-i18next';
import { getProcessedResourceBundle } from '../../quick-starts/utils/quick-start-context';
import { LAST_LANGUAGE_LOCAL_STORAGE_KEY } from './const';
import { getLastLanguage } from './getLastLanguage';

export const useLanguage = (preferredLanguage: string, preferredLanguageLoaded: boolean) => {
const { i18n } = useTranslation();
const { setResourceBundle } = React.useContext<QuickStartContextValues>(QuickStartContext);

React.useEffect(() => {
const onLanguageChange = (lng: string) => {
if (setResourceBundle) {
// Update language resource of quick starts components
const resourceBundle = i18n.getResourceBundle(lng, 'console-app');
const processedBundle = getProcessedResourceBundle(resourceBundle, lng);
setResourceBundle(processedBundle, lng);
}
};
const preferredLanguageInStorage: string = getLastLanguage();

i18n.on('languageChanged', onLanguageChange);

if (preferredLanguageLoaded && preferredLanguage !== preferredLanguageInStorage) {
if (preferredLanguage) {
localStorage.setItem(LAST_LANGUAGE_LOCAL_STORAGE_KEY, preferredLanguage);
i18n.changeLanguage(preferredLanguage);
} else {
preferredLanguageInStorage && localStorage.removeItem(LAST_LANGUAGE_LOCAL_STORAGE_KEY);
}
}

return () => {
i18n.off('languageChanged', onLanguageChange);
};
}, [i18n, preferredLanguage, preferredLanguageLoaded, setResourceBundle]);
};
@@ -1,3 +1,4 @@
import * as React from 'react';
import { useUserSettingsCompatibility } from '@console/shared/src/hooks/useUserSettingsCompatibility';

const PREFERRED_LANGUAGE_USER_SETTING_KEY = 'console.preferredLanguage';
Expand All @@ -11,4 +12,6 @@ export const usePreferredLanguage = (): [
useUserSettingsCompatibility<string>(
PREFERRED_LANGUAGE_USER_SETTING_KEY,
PREFERRED_LANGUAGE_LOCAL_STORAGE_KEY,
null,
true,
);