🏡 index : quine-global/quine-anaconda-webui.git

/*
 * Copyright (C) 2022 Red Hat, Inc.
 * SPDX-License-Identifier: LGPL-2.1-or-later
 */

import cockpit from "cockpit";

import React, { useContext, useEffect, useState } from "react";
import { Form, FormGroup } from "@patternfly/react-core/dist/esm/components/Form/index.js";

import { setLocale } from "../../apis/boss.js";
import {
    setLanguage,
} from "../../apis/localization.js";

import {
    convertToCockpitLang,
    getLangCookie,
    setLangCookie
} from "../../helpers/language.js";
import { error as loggerError } from "../../helpers/log.js";

import { LanguageContext, PageContext, SystemTypeContext } from "../../contexts/Common.jsx";

import { MenuSearch } from "../common/MenuSearch.jsx";
import { Keyboard } from "./Keyboard.jsx";

import "./InstallationLanguage.scss";

const _ = cockpit.gettext;
const SCREEN_ID = "anaconda-screen-language";
const error = loggerError.bind(null, SCREEN_ID + ":");

const getLanguageEnglishName = lang => lang["english-name"].v;
const getLanguageNativeName = lang => lang["native-name"].v;
const getLocaleId = locale => locale["locale-id"].v;
const getLocaleNativeName = locale => locale["native-name"].v;

class LanguageSelector extends React.Component {
    static contextType = PageContext;

    constructor (props) {
        super(props);
        this.initiallySelectedLanguage = props.language;

        this.renderOptions = this.renderOptions.bind(this);
    }

    componentDidMount () {
        try {
            const cockpitLang = convertToCockpitLang({ lang: this.props.language });
            if (getLangCookie() !== cockpitLang) {
                setLangCookie({ cockpitLang });
                window.location.reload(true);
            }
            setLocale({ locale: this.props.language });
        } catch (e) {
            this.context?.setStepNotification?.({
                step: SCREEN_ID,
                ...e,
            });
        }
    }

    renderOptions () {
        const { commonLocales, languages } = this.props;
        const options = [];

        // Returns a locale with a given code.
        const findLocaleWithId = (localeCode) => {
            for (const languageId in languages) {
                const languageItem = languages[languageId];
                for (const locale of languageItem.locales) {
                    if (getLocaleId(locale) === localeCode) {
                        return locale;
                    }
                }
            }
            error(`Locale with code ${localeCode} not found.`);
        };

        const onSearch = (locale, search) => {
            const searchLower = search.toLowerCase();

            return (
                getLocaleNativeName(locale).toLowerCase()
                        .includes(searchLower) ||
                getLanguageNativeName(locale).toLowerCase()
                        .includes(searchLower) ||
                getLanguageEnglishName(locale).toLowerCase()
                        .includes(searchLower)
            );
        };

        // Helper to create a menu item
        const createMenuItem = (locale, prefix) => {
            return ({
                id: `${SCREEN_ID}-language-${prefix}-${getLocaleId(locale).split(".UTF-8")[0]}`,
                item: locale,
                itemId: getLocaleId(locale),
                itemLang: convertToCockpitLang({ lang: getLocaleId(locale) }),
                itemText: getLocaleNativeName(locale),
                itemType: "menu-item",
                key: `${prefix}-${getLocaleId(locale)}`,
                onSearch: onSearch.bind(null, locale),
            });
        };

        const suggestedItems = commonLocales
                .map(findLocaleWithId)
                .filter(locale => locale !== undefined)
                .sort((a, b) => {
                    // Sort alphabetically by native name but keep the default locale at the top
                    if (getLocaleId(a) === this.initiallySelectedLanguage) {
                        return -1;
                    } else if (getLocaleId(b) === this.initiallySelectedLanguage) {
                        return 1;
                    }
                    return getLocaleNativeName(a).localeCompare(getLocaleNativeName(b));
                })
                .map(locale => createMenuItem(locale, "option-common"));

        if (suggestedItems.length > 0) {
            options.push({
                id: `${SCREEN_ID}-common-languages`,
                itemChildren: suggestedItems,
                itemLabel: _("Suggested languages"),
                itemLabelHeadingLevel: "h3",
                itemType: "menu-group",
            });
        }

        const otherItems = Object.keys(languages)
                .sort((a, b) => {
                    return getLanguageNativeName(languages[a].locales[0]).localeCompare(getLanguageNativeName(languages[b].locales[0]));
                })
                .flatMap(languageId => {
                    const languageItem = languages[languageId];
                    return languageItem.locales;
                })
                .filter(locale => commonLocales.indexOf(getLocaleId(locale)) === -1)
                .map(locale => createMenuItem(locale, "option-alpha"));

        if (otherItems.length > 0) {
            options.push({
                id: `${SCREEN_ID}-additional-languages`,
                itemChildren: otherItems,
                itemLabel: _("Additional languages"),
                itemLabelHeadingLevel: "h3",
                itemType: "menu-group",
            });
        }

        return options;
    }

    render () {
        const { languages } = this.props;

        const handleOnSelect = (_event, item) => {
            for (const languageItem in languages) {
                for (const localeItem of languages[languageItem].locales) {
                    if (getLocaleId(localeItem) === item) {
                        setLangCookie({ cockpitLang: convertToCockpitLang({ lang: getLocaleId(localeItem) }) });
                        setLanguage({ lang: getLocaleId(localeItem) })
                                .then(() => setLocale({ locale: getLocaleId(localeItem) }))
                                .catch(ex => {
                                    this.context?.setStepNotification?.({
                                        step: SCREEN_ID,
                                        ...ex,
                                    });
                                });
                        this.setState({ lang: item });
                        fetch("po.js").then(response => response.text())
                                .then(body => {
                                    // always reset old translations
                                    cockpit.locale(null);
                                    // en_US is always null
                                    if (body.trim() === "") {
                                        cockpit.locale({
                                            "": {
                                                language: "en_US",
                                                "language-direction": "ltr",
                                            }
                                        });
                                    } else {
                                        // eslint-disable-next-line no-eval
                                        eval(body);
                                    }

                                    const langEvent = new CustomEvent("cockpit-lang");
                                    window.dispatchEvent(langEvent);
                                });
                        return;
                    }
                }
            }
        };

        const options = this.renderOptions();

        return (
            <MenuSearch
              ariaLabelSearch={_("Search for a language")}
              handleOnSelect={handleOnSelect}
              menuType="language"
              options={options}
              screenId={SCREEN_ID}
              selection={this.props.language}
            />
        );
    }
}

export const InstallationLanguage = ({ dispatch }) => {
    const { setIsFormValid } = useContext(PageContext) ?? {};
    const { commonLocales, keyboardLayouts, language, languages } = useContext(LanguageContext);
    const { desktopVariant } = useContext(SystemTypeContext);
    const isGnome = desktopVariant === "GNOME";
    const [isLanguageValid, setIsLanguageValid] = useState(false);
    const [isKeyboardValid, setIsKeyboardValid] = useState(false);

    useEffect(() => {
        dispatch({ type: "SET_LANGUAGE_USER_CONFIGURED" });
    }, [dispatch]);

    useEffect(() => {
        setIsLanguageValid(language !== "");
    }, [language]);

    useEffect(() => {
        setIsFormValid(isLanguageValid && isKeyboardValid);
    }, [isLanguageValid, isKeyboardValid, setIsFormValid]);

    return (
        <>
            <Form isHorizontal>
                <FormGroup
                  className="anaconda-screen-selectors-container"
                  label={_("Language")}
                >
                    <LanguageSelector
                      id="language-selector"
                      languages={languages}
                      commonLocales={commonLocales}
                      language={language}
                    />
                </FormGroup>

                {keyboardLayouts.length > 0 && (
                    <FormGroup
                      className={!isGnome ? "anaconda-screen-selectors-container" : ""}
                      fieldId={`${SCREEN_ID}-keyboard-layouts`}
                      label={_("Keyboard")}
                    >
                        <Keyboard
                          dispatch={dispatch}
                          isGnome={isGnome}
                          setIsKeyboardValid={setIsKeyboardValid}
                        />
                    </FormGroup>
                )}
            </Form>
        </>
    );
};