Skip to content

[WEB-4294] Persist language choice across pages #2500

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

Merged
merged 4 commits into from
Mar 31, 2025
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 15 additions & 28 deletions src/components/LanguageButton/LanguageButton.test.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { LayoutProvider } from 'src/contexts/layout-context';

import { PageLanguageContext } from 'src/contexts/page-language-context';
import LanguageButton from './LanguageButton';

const contextValue = {
handleCurrentLanguageChange: jest.fn(),
getPreferredLanguage: jest.fn(),
setPreferredLanguage: jest.fn(),
};
jest.mock('@reach/router', () => ({
useLocation: () => ({
pathname: '/test-path',
search: '?lang=javascript',
}),
}));

describe(`<LanguageButton />`, () => {
it('renders default state button', () => {
Expand All @@ -26,9 +26,9 @@ describe(`<LanguageButton />`, () => {
});
it('renders active state button', () => {
render(
<PageLanguageContext.Provider value={{ currentLanguage: 'javascript', ...contextValue }}>
<LayoutProvider>
<LanguageButton language="javascript" selectedSDKInterfaceTab="realtime" selectedLocalLanguage="javascript" />
</PageLanguageContext.Provider>,
</LayoutProvider>,
);
expect(screen.getByRole('button')).toMatchInlineSnapshot(`
<button
Expand All @@ -39,28 +39,15 @@ describe(`<LanguageButton />`, () => {
`);
});

it('changes session storage value on click', async () => {
const setItemSpy = jest.spyOn(contextValue, 'setPreferredLanguage');
render(
<PageLanguageContext.Provider value={{ currentLanguage: 'python', ...contextValue }}>
<LanguageButton selectedSDKInterfaceTab="realtime" language="javascript" selectedLocalLanguage="javascript" />
</PageLanguageContext.Provider>,
);

const button = screen.getByRole('button');
await userEvent.click(button);
expect(setItemSpy).toHaveBeenCalledWith('javascript');
});

it('renders active button if pageLanguage is not in the languages but the language is the first language of the array', () => {
render(
<PageLanguageContext.Provider value={{ currentLanguage: 'php', ...contextValue }}>
<LayoutProvider>
<LanguageButton language="ruby" selectedSDKInterfaceTab="realtime" selectedLocalLanguage="ruby" />
</PageLanguageContext.Provider>,
</LayoutProvider>,
);
expect(screen.getByRole('button')).toMatchInlineSnapshot(`
<button
class="button ui-text-menu3 isActive"
class="button ui-text-menu3"
>
Ruby
</button>
Expand All @@ -69,13 +56,13 @@ describe(`<LanguageButton />`, () => {

it('renders active button if language is a sdk interface', () => {
render(
<PageLanguageContext.Provider value={{ currentLanguage: 'java', ...contextValue }}>
<LayoutProvider>
<LanguageButton language="rest_java" selectedSDKInterfaceTab="rest" selectedLocalLanguage="java" />
</PageLanguageContext.Provider>,
</LayoutProvider>,
);
expect(screen.getByRole('button')).toMatchInlineSnapshot(`
<button
class="button ui-text-menu3 isActive"
class="button ui-text-menu3"
/>
`);
});
Expand Down
10 changes: 5 additions & 5 deletions src/components/LanguageButton/LanguageButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,26 @@ import cn from '@ably/ui/core/utils/cn';
import { createLanguageHrefFromDefaults, getLanguageDefaults, getTrimmedLanguage } from 'src/components';
import { LanguageNavigationComponentProps } from '../Menu/LanguageNavigation';
import { button, isActive } from '../Menu/MenuItemButton/MenuItemButton.module.css';
import { usePageLanguage } from 'src/contexts';
import { languageInfo } from 'src/data/languages';
import { LanguageKey } from 'src/data/languages/types';
import { useLayoutContext } from 'src/contexts/layout-context';

const LanguageButton: FC<LanguageNavigationComponentProps> = ({ language, selectedLocalLanguage }) => {
const { currentLanguage: pageLanguage, setPreferredLanguage } = usePageLanguage();
const { activePage, setLanguage } = useLayoutContext();
const selectedLanguage = getTrimmedLanguage(language);
const { isLanguageDefault, isPageLanguageDefault } = getLanguageDefaults(selectedLanguage, pageLanguage);
const { isLanguageDefault, isPageLanguageDefault } = getLanguageDefaults(selectedLanguage, activePage.language);
/*
separate the isLanguageActive variable because we will pass a pageLanguage value that is not always from the useContext(PageLanguageContext),
so if the useContext(PageLanguageContext) is not present in the languages we will pass the first language of the languages
eg: selected global language: PHP but the languages are: [js, ruby] so it will pass js now as php is not present
*/
const { isLanguageActive } = getLanguageDefaults(selectedLanguage, selectedLocalLanguage);
const isLanguageActive = activePage.language === selectedLanguage;

const handleClick = () => {
const href = createLanguageHrefFromDefaults(isPageLanguageDefault, isLanguageDefault, selectedLanguage);

if (!isPageLanguageDefault) {
setPreferredLanguage(language);
setLanguage(language);
}
navigate(href);
};
Expand Down
7 changes: 0 additions & 7 deletions src/components/Layout/LanguageSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,13 +99,6 @@ export const LanguageSelector = () => {
const queryParams = useMemo(() => new URLSearchParams(location.search), [location.search]);
const langParam = queryParams.get('lang');

useEffect(() => {
if (langParam && !options.some((option) => option.label === langParam)) {
queryParams.delete('lang');
navigate(`${location.pathname}?${queryParams.toString()}`, { replace: true });
}
}, [langParam, options, location.pathname, queryParams]);

useEffect(() => {
const defaultOption = options.find((option) => option.label === langParam) || options[0];
setSelectedOption(defaultOption);
Expand Down
13 changes: 11 additions & 2 deletions src/components/Layout/utils/nav.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,17 @@ import { HEADER_HEIGHT, componentMaxHeight } from '@ably/ui/core/utils/heights';
import { ProductData, ProductKey } from 'src/data/types';
import { NavProductContent, NavProductPage, NavProductPages } from 'src/data/nav/types';
import { LanguageKey } from 'src/data/languages/types';
import { DEFAULT_LANGUAGE } from 'src/contexts/layout-context';

export type PageTreeNode = { index: number; page: NavProductPage };

export type ActivePage = { tree: PageTreeNode[]; page: NavProductPage; languages: LanguageKey[] };
export type ActivePage = {
tree: PageTreeNode[];
page: NavProductPage;
languages: LanguageKey[];
language: LanguageKey;
product: ProductKey | null;
};

/**
* Determines the active page based on the provided target link.
Expand Down Expand Up @@ -64,6 +71,8 @@ export const determineActivePage = (data: ProductData, targetLink: string): Acti
tree: [{ index: Object.keys(data).indexOf(key), page: { name, link } }],
page: { name, link },
languages: [],
language: DEFAULT_LANGUAGE,
product: key,
};
}

Expand Down Expand Up @@ -94,7 +103,7 @@ export const determineActivePage = (data: ProductData, targetLink: string): Acti
data[key].nav[apiResult ? 'api' : 'content'],
);

return { tree, page: page?.[0] as NavProductPage, languages: [] };
return { tree, page: page?.[0] as NavProductPage, languages: [], language: DEFAULT_LANGUAGE, product: key };
}
}
}
Expand Down
7 changes: 3 additions & 4 deletions src/components/Link/LanguageLink.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { Link } from 'gatsby';
import { usePageLanguage } from '../../contexts/page-language-context';
import { getLanguageDefaults } from '../common/language-defaults';
import { languageLabel } from 'src/data/languages';
import { LanguageKey } from 'src/data/languages/types';

import { useLayoutContext } from 'src/contexts/layout-context';
const LanguageLink = ({ language }: { language: LanguageKey }) => {
const { currentLanguage: pageLanguage } = usePageLanguage();
const { activePage } = useLayoutContext();

const { isLanguageDefault, isPageLanguageDefault } = getLanguageDefaults(language, pageLanguage);
const { isLanguageDefault, isPageLanguageDefault } = getLanguageDefaults(language, activePage.language);
const href = isPageLanguageDefault
? `./language/${language}`
: `../../${isLanguageDefault ? '' : `language/${language}`}`;
Expand Down
9 changes: 5 additions & 4 deletions src/components/Menu/LanguageNavigation/LanguageNavigation.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Dispatch, FunctionComponent as FC, SetStateAction } from 'react';
import { usePageLanguage } from 'src/contexts';
import { SingleValue } from 'react-select';
import { navigate } from 'gatsby';

Expand All @@ -15,6 +14,7 @@ import {
Select,
} from 'src/components';
import cn from '@ably/ui/core/utils/cn';
import { useLayoutContext } from 'src/contexts/layout-context';

export interface LanguageNavigationComponentProps {
language: string;
Expand Down Expand Up @@ -65,12 +65,13 @@ const LanguageNavigation = ({
setSelectedSDKInterfaceTab,
setPreviousSDKInterfaceTab,
}: LanguageNavigationProps) => {
const { currentLanguage: pageLanguage, setPreferredLanguage } = usePageLanguage();
const selectedPageLanguage = pageLanguage === DEFAULT_LANGUAGE ? DEFAULT_PREFERRED_LANGUAGE : pageLanguage;
const { activePage, setLanguage } = useLayoutContext();
const selectedPageLanguage =
activePage.language === DEFAULT_LANGUAGE ? DEFAULT_PREFERRED_LANGUAGE : activePage.language;
const options = items.map((item) => ({ label: item.content, value: item.props.language }));
const value = options.find((option) => option.value === selectedPageLanguage);

const onSelectChange = changePageOnSelect(pageLanguage, setPreferredLanguage);
const onSelectChange = changePageOnSelect(activePage.language, setLanguage);

const isSDKInterFacePresent = allListOfLanguages
? checkIfLanguageHasSDKInterface(allListOfLanguages, SDK_INTERFACES)
Expand Down
13 changes: 10 additions & 3 deletions src/components/blocks/Html/Html.test.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
import React from 'react';
import { render } from '@testing-library/react';
import { DlWrapper } from 'src/components/blocks/list/Dl/DlWrapper';
import { PageLanguageContext } from 'src/contexts';
import { LayoutProvider } from 'src/contexts/layout-context';

import Html from './';
import { dtFixture } from './fixtures';

jest.mock('@reach/router', () => ({
useLocation: () => ({
pathname: '/test-path',
search: '?lang=python',
}),
}));

describe('<Html />', () => {
it('renders correct Dl based on PageLanguageContext value', () => {
const { container } = render(
<PageLanguageContext.Provider value="python">
<LayoutProvider>
<Html data={dtFixture} BlockWrapper={DlWrapper} />
</PageLanguageContext.Provider>,
</LayoutProvider>,
);
expect(container).toMatchSnapshot();
});
Expand Down
10 changes: 7 additions & 3 deletions src/components/blocks/Html/LinkableHtmlBlock.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import PropTypes from 'prop-types';
import { usePageLanguage } from 'src/contexts';
import Html from '.';
import CopyLink from '../wrappers/CopyLink';
import { childOrSelfHasLanguageMatchingPageLanguageOrDefault } from '../wrappers/language-utilities';
import { useLayoutContext } from 'src/contexts/layout-context';

const LinkableHtmlBlock = (Type, marginBottom, marginTop) => {
const InnerBlock = ({ data, attribs }) => {
const { currentLanguage: pageLanguage } = usePageLanguage();
const shouldShowBlock = childOrSelfHasLanguageMatchingPageLanguageOrDefault(pageLanguage, data, attribs?.lang);
const { activePage } = useLayoutContext();
const shouldShowBlock = childOrSelfHasLanguageMatchingPageLanguageOrDefault(
activePage.language,
data,
attribs?.lang,
);
if (shouldShowBlock) {
return (
<CopyLink attribs={attribs} marginBottom={marginBottom} marginTop={marginTop}>
Expand Down
57 changes: 56 additions & 1 deletion src/components/blocks/Html/__snapshots__/Html.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,75 @@ exports[`<Html /> renders correct Dl based on PageLanguageContext value 1`] = `
>
event name for the published message
<span
lang="default"
lang="python"
>
<em>
Type:
<code
class="ui-text-code-inline"
>
Unicode
</code>
for Python 2,
<code
class="ui-text-code-inline"
>
String
</code>
for Python 3
</em>
</span>
</dd>


<div
lang="python"
>
<dt
class="listDt"
>
<div>
data
</div>
</dt>
<dd
class=""
>
data payload for the message. The supported payload types are unicode Strings, Dict, or List objects that can be serialized to
<span
class="caps"
>
JSON
</span>
using
<code
class="ui-text-code-inline"
>
json.dumps
</code>
, binary data as
<code
class="ui-text-code-inline"
>
bytearray
</code>
(in Python 3,
<code
class="ui-text-code-inline"
>
bytes
</code>
also works), and None.
<em>
Type:
<code
class="ui-text-code-inline"
>
Object
</code>
</em>
</dd>
</div>



Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { matchesLanguageOrDefault } from '../../wrappers/language-utilities';
import { usePageLanguage } from 'src/contexts';
import Html from '../../Html';
import { HtmlComponentProps, HtmlComponentPropsData } from 'src/components/html-component-props';
import { isString } from 'lodash/fp';
import { useLayoutContext } from 'src/contexts/layout-context';

const DIV_CONTENTS_WHICH_SHOULD_IGNORE_NORMAL_LANGUAGE_RULES = ['dt', 'dd'];

Expand All @@ -17,9 +17,11 @@ const childExistsOfIgnoredType = (data: HtmlComponentPropsData) =>
: false;

const ApiReferenceDiv = ({ data, attribs }: HtmlComponentProps<'div'>) => {
const { currentLanguage: pageLanguage } = usePageLanguage();
const { activePage } = useLayoutContext();
const shouldShowBlock =
attribs?.forcedisplay || matchesLanguageOrDefault(pageLanguage, attribs?.lang) || childExistsOfIgnoredType(data);
attribs?.forcedisplay ||
matchesLanguageOrDefault(activePage.language, attribs?.lang) ||
childExistsOfIgnoredType(data);

return shouldShowBlock ? (
<div {...attribs}>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { HtmlComponentProps } from 'src/components/html-component-props';
import { usePageLanguage } from 'src/contexts';
import Html from '../../Html';
import { matchesLanguageOrDefault } from '../../wrappers/language-utilities';
import { useLayoutContext } from 'src/contexts/layout-context';

const ApiReferenceSpan = ({ data, attribs }: HtmlComponentProps<'span'>) => {
const { currentLanguage: pageLanguage } = usePageLanguage();
const shouldShowBlock = matchesLanguageOrDefault(pageLanguage, attribs?.lang);
const { activePage } = useLayoutContext();
const shouldShowBlock = matchesLanguageOrDefault(activePage.language, attribs?.lang);

return shouldShowBlock ? (
<span {...attribs}>
Expand Down
Loading