mui / mui-x

MUI X: Build complex and data-rich applications using a growing list of advanced React components, like the Data Grid, Date and Time Pickers, Charts, and more!
https://mui.com/x/
3.88k stars 1.17k forks source link

[data grid] how to update column header titles when switching language without reseting data-grid state #13055

Open layerok opened 1 month ago

layerok commented 1 month ago

The problem in depth

I know that columns prop should keep the same reference between two rerenders to avoid loosing data grid state. But If I don't update columns prop when switching language then column headers aren't translated. If I update columns prop then data grid state is reset. What should I do in this situation?

Link to sandbox

Steps:

  1. resize some column
  2. change language

https://github.com/mui/mui-x/assets/18424848/c3513836-210d-46fe-a1cc-f54c6e2af806

Your environment

No response

Search keywords: i18next translation column language data grid Order ID: 74777

layerok commented 1 month ago

well I found something

instead of doing this

const { t } = useTranslation();
const columns = useMemo(() => [
  {
    field: "first_name",
    headerName: t("first_name")
  },
  {
    field: "last_name",
    headerName: t("last_name")
  },
], [t]);

I can do this

const columns = useMemo(() => [
  {
    field: "first_name",
    renderHeader: () => <Trans i18nKey="first_name"/>
  },
  {
    field: "last_name",
    renderHeader: () => <Trans i18nKey="last_name"/>
  },
], []);
layerok commented 1 month ago

Title of a custom filter operator also doesn't update when switching languages. But if you pass the Trans component instead of a simple string, it will update

const { t } = useTranslation();
const columns = useMemo(() => [
  {
    field: "first_name",
    renderHeader: () => <Trans i18nKey="first_name"/>,
    filterable: true,
    filterOperators: [
      {
        value: "contains",
        getApplyFilterFn: () => null,
        // @ts-ignore
        label: <Trans i18nKey="header-operators.mode.contains"/>, // <-- it will update when switching languages
        // @ts-ignore
        headerLabel: <Trans i18nKey="header-operators.mode.contains"/>, // <-- it will update when switching languages
      },
      {
        value: "exact",
        getApplyFilterFn: () => null,
        label: t("header-operators.mode.exact"), // <-- it won't update when switching languages
        headerLabel: t("header-operators.mode.exact"), // <-- it won't update when switching languages
      },
      // ... other filter operators
    ]
  },
  // ... other column definitions 
], []);

I had to use ts-ignore because typescript is unhappy about Trans being passed to a property that accepts only a value of type string

michelengelen commented 1 month ago

Hey @layerok ... Sry for the late reply. I had some time off. It seems you solved your issue already. Is there anything else we can help you with?

layerok commented 1 month ago

@michelengelen I am not satisfied with my solution Firstly, I don't like using ts-ignore Secondly, I don't like using renderHeader because I can't just return translation in the renderHeader callback because then the DataGrid will render the unstyled header. I need to wrap it in some component first. And this wrapping makes column config larger and harder to maintain.

renderHeader: () => <HeaderCell><Trans i18nKey="some-key"/></HeaderCell>

I think this problem requires solution at the library level How I see a perfect solution

const columns = useMemo([
  {
    field: 'title',
    headerName: () => t('title'),
    filterOperators: [
      {
        label: () => t('contains'),
        headerLabel: () => t('contains')
      }
    ]
  }
], []);

const apiRef = useGridApiRef();

useLayoutEffect(() => {
  const forceUpdate = () => {
    apiRef.current.forceUpdate();
    apiRef.current.updateColumns(columns)
  };
  i18n.on("languageChanged", forceUpdate);
  return () => i18n.off("languageChanged", forceUpdate);
}, [apiRef, columns]);

return <DataGrid columns={columns} />

but right now this is impossible because headerName doesn't accept a callback value

I guess this issue is becoming more of a feature request than a question

layerok commented 1 month ago

Workaround 2

const columnsRef = useRef<ExtendedGridColDef[]>([
  {
    field: 'title',
    lazyHeaderName: () => t('title'),
    headerName: "",
  }
], []);

const apiRef = useGridApiRef();

useLayoutEffect(() => {
  const forceUpdate = () => {
    apiRef.current.forceUpdate();
    apiRef.current.updateColumns(columnsRef.current.map((column) => ({
      ...column,
      headerName: column.lazyHeaderName()
    }));
  };
  i18n.on("languageChanged", forceUpdate);
  return () => i18n.off("languageChanged", forceUpdate);
}, [apiRef, columnsRef.current]);

return <DataGrid columns={columnsRef.current}/>
michelengelen commented 1 month ago

I am not sure where the problem is: This does work for me.

const translations = {
  first_name: {
    en: 'First Name',
    de: 'Vorname',
  },
  last_name: {
    en: 'Last Name',
    de: 'Nachname',
  },
};

export default function DataGridDemo() {
  const [lang, setLang] = React.useState<'en' | 'de'>('en');
  // dummy function to just return some values from an object
  const t = (key: keyof typeof translations) => translations[key][lang];

  const HeaderCell = ({ labelKey }: { labelKey: keyof typeof translations }) => (
    <div>{t(labelKey)}</div>
  );

  const columns = React.useMemo(
    () => [
      { field: 'id', headerName: 'ID', width: 90 },
      {
        field: 'firstName',
        width: 150,
        editable: true,
        renderHeader: () => <HeaderCell labelKey="first_name" />,
      },
      {
        field: 'lastName',
        width: 150,
        editable: true,
        renderHeader: () => <HeaderCell labelKey="last_name" />,
      },
    ],
    [t],
  );
...

Wouldn't this be sufficient?

This does work like as well when using:

const columns = React.useMemo(
  () => [
    { field: 'id', headerName: 'ID', width: 90 },
    {
      field: 'firstName',
      headerName: t('first_name'),
      width: 150,
      editable: true,
    },
    {
      field: 'lastName',
      headerName: t('last_name'),
      width: 150,
      editable: true,
    },
  ],
  [t],
);

Is i18n not updating the t function on a language change?

michelengelen commented 1 month ago

I would need setup a dev environment with next for this. This might take until tomorrow though. Would that be ok?

layerok commented 1 month ago

Yes, and you can use my sandbox. next is already there

layerok commented 1 month ago

Is i18n not updating the t function on a language change?

The problem is not with i18next but with the columns prop. The columns prop must keep the reference between two rerenders. Otherwise, the datagrid state will be reset So putting the t function into dependency array of useMemo is not an option.

const columns = React.useMemo(() => [{ field: 'id', headerName: t('id'), width: 90 }], [t]);

If you don't put the t function into the dependency array then the header won't be updated when the language changes We can use renderHeader to workaround this problem, but I've already explained why I don't like this solution. And also what to do with filterOperator label, it doesn't have a renderLabel option.

michelengelen commented 1 month ago

OK, thanks ... got it. It does indeed look as if there is no satisfying solution (afaik) to this. I will add this to the board and see what the team has to say about it.

flaviendelangle commented 1 month ago

You could probably use apiRef.updateColumns and pass the new headers, but it's not the prettier solution imaginable.