Skip to content
4 changes: 4 additions & 0 deletions packages/pxweb2-ui/src/lib/shared-types/pxTableMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ export type PxTableMetadata = {
* Table language.
*/
language: string;
/**
* The available languages for the table.
*/
availableLanguages: string[];
/**
* A title for the table that describes the content of it.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import classes from './LanguageSwitcher.module.scss';
let currentPathname = '/en/tables';
const navigateMock = vi.fn();
let isMobile = false;
let appLanguageFilter: string[] = [];
let mockI18nLanguage = 'en';
const changeLanguageMock = vi.fn();

Expand Down Expand Up @@ -43,7 +44,7 @@ vi.mock('@pxweb2/pxweb2-ui', () => ({
}));

vi.mock('../../context/useApp', () => ({
default: () => ({ isMobile }),
default: () => ({ isMobile, languageFilter: appLanguageFilter }),
}));

// Override the global i18n mock for this test file to add controllable behavior
Expand All @@ -64,6 +65,7 @@ describe('LanguageSwitcher', () => {
changeLanguageMock.mockClear();
currentPathname = '/en/tables';
isMobile = false;
appLanguageFilter = [];
mockI18nLanguage = 'en';
});

Expand Down Expand Up @@ -256,4 +258,32 @@ describe('LanguageSwitcher', () => {

expect(select.value).toBe('sv');
});

it('enables all languages when app language filter is empty', () => {
appLanguageFilter = [];

render(<LanguageSwitcher />);

const options = screen.getAllByRole('option') as HTMLOptionElement[];

expect(options).toHaveLength(4);
expect(options.every((option) => !option.disabled)).toBe(true);
});

it('only displays filtered languages when app language filter has values', () => {
appLanguageFilter = ['en', 'sv'];

render(<LanguageSwitcher />);

const options = screen.getAllByRole('option') as HTMLOptionElement[];
const optionByValue = Object.fromEntries(
options.map((option) => [option.value, option]),
);

expect(options).toHaveLength(2);
expect(optionByValue.en).toBeDefined();
expect(optionByValue.sv).toBeDefined();
expect(optionByValue.no).toBeUndefined();
expect(optionByValue.ar).toBeUndefined();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import classes from './LanguageSwitcher.module.scss';

export const LanguageSwitcher = () => {
const { t, i18n } = useTranslation();
const isMobile = useApp().isMobile;
const { isMobile, languageFilter: appLanguageFilter = [] } = useApp();
const config = getConfig();
const navigate = useNavigate();
const location = useLocation();
Expand All @@ -26,6 +26,13 @@ export const LanguageSwitcher = () => {
setCurrentLang(i18n.language);
}, [location.pathname, i18n.language]);

const hasLanguageFilter = appLanguageFilter.length > 0;
const languageOptions = hasLanguageFilter
? config.language.supportedLanguages.filter((language) =>
appLanguageFilter.includes(language.shorthand),
)
: config.language.supportedLanguages;

const handleLanguageChange = (
event: React.ChangeEvent<HTMLSelectElement>,
) => {
Expand Down Expand Up @@ -108,7 +115,7 @@ export const LanguageSwitcher = () => {
}}
onChange={(event) => handleLanguageChange(event)}
>
{config.language.supportedLanguages.map((language) => (
{languageOptions.map((language) => (
<option
key={language.shorthand}
lang={language.shorthand}
Expand Down
12 changes: 12 additions & 0 deletions packages/pxweb2/src/app/components/Selection/Selection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import useAccessibility from '../../context/useAccessibility';
import { problemMessage } from '../../util/problemMessage';
import {
getSelectedCodelists,
mapAvailableLanguagesFromLinks,
updateSelectedCodelistForVariable,
} from './selectionUtils';

Expand Down Expand Up @@ -307,6 +308,12 @@ export function Selection({
.then(([Dataset, TableData]) => {
const pxTable: PxTable = mapJsonStat2Response(Dataset, false);

pxTable.metadata.availableLanguages = mapAvailableLanguagesFromLinks(
TableData.links,
);

app.setLanguageFilter(pxTable.metadata.availableLanguages);

const firstMatchingPathArray = TableData.paths?.find(
(pathArr: PathElement[]) => pathArr[0]?.id === TableData.subjectCode,
);
Expand Down Expand Up @@ -455,6 +462,11 @@ export function Selection({
pxTable.metadata.pathElements = preservedPathElements;
}

// Preserve available languages because codelist metadata response
// does not include table links.
pxTable.metadata.availableLanguages =
pxTableMetaToRender?.availableLanguages ?? [];

setPxTableMetadata(pxTable.metadata);

if (pxTableMetaToRender !== null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
updateSelectedCodelistForVariable,
addSelectedCodeListToVariable,
getSelectedCodelists,
mapAvailableLanguagesFromLinks,
} from './selectionUtils';
import {
SelectedVBValues,
Expand Down Expand Up @@ -356,4 +357,50 @@ describe('selectionUtils', () => {
});
});
});

describe('mapAvailableLanguagesFromLinks', () => {
it('maps hreflang from self and alternate links only', () => {
const result = mapAvailableLanguagesFromLinks([
{
rel: 'self',
hreflang: 'en',
href: 'https://example.org/table?lang=en',
},
{
rel: 'alternate',
hreflang: 'sv',
href: 'https://example.org/table?lang=sv',
},
{
rel: 'metadata',
hreflang: 'no',
href: 'https://example.org/table/metadata?lang=no',
},
]);

expect(result).toEqual(['en', 'sv']);
});

it('removes duplicate hreflang values', () => {
const result = mapAvailableLanguagesFromLinks([
{
rel: 'self',
hreflang: 'en',
href: 'https://example.org/table?lang=en',
},
{
rel: 'alternate',
hreflang: 'en',
href: 'https://example.org/table?lang=en',
},
]);

expect(result).toEqual(['en']);
});

it('returns empty array when links are missing', () => {
expect(mapAvailableLanguagesFromLinks(undefined)).toEqual([]);
expect(mapAvailableLanguagesFromLinks(null)).toEqual([]);
});
});
});
15 changes: 15 additions & 0 deletions packages/pxweb2/src/app/components/Selection/selectionUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,21 @@ import {
Variable,
PxTableMetadata,
} from '@pxweb2/pxweb2-ui';
import { Link } from '@pxweb2/pxweb2-api-client';

export function mapAvailableLanguagesFromLinks(
links: Link[] | null | undefined,
): string[] {
const relevantRels = new Set(['self', 'alternate']);

return Array.from(
new Set(
(links ?? [])
.filter((link) => relevantRels.has(link.rel) && link.hreflang)
.map((link) => link.hreflang),
),
);
}

export function updateSelectedCodelistForVariable(
selectedItem: SelectOption,
Expand Down
12 changes: 11 additions & 1 deletion packages/pxweb2/src/app/context/AppProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export type AppContextType = {
setSkipToMainFocused: (focused: boolean) => void;
title: string;
setTitle: (title: string) => void;
languageFilter: string[];
setLanguageFilter: (languages: string[]) => void;
};

// Create the context with default values
Expand All @@ -37,6 +39,10 @@ export const AppContext = createContext<AppContextType>({
setTitle: () => {
return;
},
languageFilter: [],
setLanguageFilter: () => {
return;
},
});

// Provider component
Expand All @@ -46,7 +52,7 @@ export const AppProvider: React.FC<{ children: React.ReactNode }> = ({
const [isInitialized] = useState(true);
const [skipToMainFocused, setSkipToMainFocused] = useState(false);
const [title, setTitle] = useState<string>('');

const [languageFilter, setLanguageFilter] = useState<string[]>([]);
/**
* Keep state if window screen size is mobile, pad or desktop.
*/
Expand Down Expand Up @@ -106,6 +112,8 @@ export const AppProvider: React.FC<{ children: React.ReactNode }> = ({
setSkipToMainFocused,
title,
setTitle,
languageFilter,
setLanguageFilter,
}),
[
getSavedQueryId,
Expand All @@ -118,6 +126,8 @@ export const AppProvider: React.FC<{ children: React.ReactNode }> = ({
setSkipToMainFocused,
title,
setTitle,
languageFilter,
setLanguageFilter,
],
);

Expand Down
Loading