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

Internationalization support #215

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,41 @@ customStyles={{
}}
```

## Internationalization


### Predefined languages
- Out-of-the-box support for various languages, having English as default.
- Common languages are readily available for selection through the new language prop (e.g., `language="fr"` for French).
- Available predefined languages:
- en
- es
- fr

### Customizable language
- For maximum flexibility, language keys can be exported and overridden. This removes the need for the project to support all languages directly.
- Users can tailor labels and rephrase messages to suit their specific needs.
- Translations keys example can be found in `src/i18n/es.ts`

```javascript
// Set up custom translations
const customTranslations: {
jp: {
Upload: "アップロード",
"Browse files": "ファイルを参照",
},
pt: {
Upload: "Carregar",
"Browse files": "Procurar arquivos",
}
}

return (
<CSVImporter language="jp" translations={customTranslations} ...props />
)

```

### showDownloadTemplateButton (_boolean_, default: `true`)
When set to `false`, hide the Download Template button on the first screen of the importer.

Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,10 @@
"@emotion/styled": "^11.11.0",
"@rollup/plugin-json": "^6.1.0",
"framer-motion": "^10.17.12",
"i18next": "^23.10.1",
"papaparse": "^5.4.1",
"react-dropzone": "^14.2.3",
"react-i18next": "^14.1.0",
"react-icons": "^4.12.0",
"xlsx": "^0.18.5",
"zustand": "^4.4.7"
Expand Down
11 changes: 11 additions & 0 deletions src/components/CSVImporter/CSVImporter.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,17 @@ const Template: Story<typeof ImporterComponent> = (args: CSVImporterProps) => {

export const Importer = Template.bind({});
Importer.args = {
language: "en",
...defaults,
template: template,
customTranslations: {
jp: {
Upload: "アップロード",
"Browse files": "ファイルを参照",
},
pt: {
Upload: "Carregar",
"Browse files": "Procurar arquivos",
},
},
};
18 changes: 17 additions & 1 deletion src/components/CSVImporter/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { forwardRef, useEffect, useRef, useState } from "react";
import { useColorMode, useStatStyles } from "@chakra-ui/react";
import { useTranslation } from "react-i18next";
import "../../i18n/i18n";
import Importer from "../../importer/features/main";
import Providers from "../../importer/providers";
import useThemeStore from "../../importer/stores/theme";
Expand All @@ -22,12 +23,27 @@ const CSVImporter = forwardRef((importerProps: CSVImporterProps, forwardRef?: an
customStyles,
showDownloadTemplateButton,
skipHeaderRowSelection,
language,
customTranslations,
...props
} = importerProps;
const ref = forwardRef ?? useRef(null);

const { t, i18n } = useTranslation();
const current = ref?.current as any;

useEffect(() => {
i18n.changeLanguage(language);
}, [language]);

useEffect(() => {
if (customTranslations) {
Object.keys(customTranslations).forEach((language) => {
i18n.addResourceBundle(language, "translation", customTranslations[language], true, true);
});
}
}, [customTranslations]);

useEffect(() => {
if (isModal && current) {
if (modalIsOpen) current?.showModal?.();
Expand Down
33 changes: 33 additions & 0 deletions src/i18n/es.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
const translations = {
Upload: "Subir",
"Select Header": "Seleccionar encabezado",
"Map Columns": "Mapear columnas",
"Expected Column": "Columnas esperadas",
Required: "Requerido",
"Drop your file here": "Suelta tu archivo aquí",
or: "o",
"Browse files": "Examinar archivos",
"Download Template": "Descargar plantilla",
"Only CSV, XLS, and XLSX files can be uploaded": "Solo se pueden subir archivos CSV, XLS y XLSX",
"Select Header Row": "Seleccionar fila de encabezado",
"Select the row which contains the column headers": "Selecciona la fila que contiene los encabezados de las columnas",
"Only the first sheet (&quot;{{sheet}}&quot;) of the Excel file will be imported. To import multiple sheets, please upload each sheet individually.": " Solo se importará la primera hoja (&quot;{{sheet}}&quot;) del archivo de Excel. Para importar varias hojas, sube cada hoja individualmente.",
"Cancel": "Cancelar",
"Continue": "Continuar",
"Your File Column": "Tu columna del archivo",
"Your Sample Data": "Tus datos de muestra",
"Destination Column": "Columna de destino",
"Include": "Incluir",
"Submit": "Enviar",
"Loading...": "Cargando...",
"Please include all required columns": "Por favor incluye todas las columnas requeridas",
"Back": "Atrás",
"- Select one -": "- Selecciona uno -",
"- empty -": "- vacío -",
"Import Successful": "Importación exitosa",
"Upload Successful": "Se ha subido con éxito",
"Done": "Listo",
"Upload another file": "Subir otro archivo",
};

export default translations;
35 changes: 35 additions & 0 deletions src/i18n/fr.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Translations in french
//TODO: Double the translations
const translations = {
Upload: "Télécharger",
"Select Header": "Sélectionner l'en-tête",
"Map Columns": "Mapper les colonnes",
"Expected Column": "Colonne attendue",
Required: "Requis",
"Drop your file here": "Déposez votre fichier ici",
or: "ou",
"Browse files": "Parcourir les fichiers",
"Download Template": "Télécharger le modèle",
"Only CSV, XLS, and XLSX files can be uploaded": "Seuls les fichiers CSV, XLS et XLSX peuvent être téléchargés",
"Select Header Row": "Sélectionner la ligne d'en-tête",
"Select the row which contains the column headers": "Sélectionnez la ligne qui contient les en-têtes de colonne",
"Only the first sheet (&quot;{{sheet}}&quot;) of the Excel file will be imported. To import multiple sheets, please upload each sheet individually.": "Seule la première feuille (&quot;{{sheet}}&quot;) du fichier Excel sera importée. Pour importer plusieurs feuilles, veuillez télécharger chaque feuille individuellement.",
"Cancel": "Annuler",
"Continue": "Continuer",
"Your File Column": "Votre colonne de fichier",
"Your Sample Data": "Vos données d'échantillon",
"Destination Column": "Colonne de destination",
"Include": "Inclure",
"Submit": "Soumettre",
"Loading...": "Chargement...",
"Please include all required columns": "Veuillez inclure toutes les colonnes requises",
"Back": "Retour",
"- Select one -": "- Sélectionnez un -",
"- empty -": "- vide -",
"Import Successful": "Importation réussie",
"Upload Successful": "Téléchargement réussi",
"Done": "Terminé",
"Upload another file": "Télécharger un autre fichier",
};

export default translations;
27 changes: 27 additions & 0 deletions src/i18n/i18n.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import i18, { Resource } from "i18next";
import { initReactI18next } from "react-i18next";
import esTranslation from "./es";
import frTranslation from "./fr";

const resources: Resource = {
en: {
translation: {},
},
fr: {
translation: frTranslation,
},
es: {
translation: esTranslation,
},
};

i18.use(initReactI18next).init({
resources,
lng: "en",
keySeparator: false,
interpolation: {
escapeValue: false,
},
});

export default i18;
66 changes: 33 additions & 33 deletions src/importer/components/UploaderWrapper/UploaderWrapper.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { useState } from "react";
import { UploaderWrapperProps } from "./types";
import { useDropzone } from "react-dropzone";
import { Box, Text } from "@chakra-ui/react";
import { useTranslation } from "react-i18next";
import { Button } from "@chakra-ui/button";
import { PiArrowCounterClockwise, PiFile } from "react-icons/pi";
import { Box, Text } from "@chakra-ui/react";
import useThemeStore from "../../stores/theme";
import { UploaderWrapperProps } from "./types";
import { PiArrowCounterClockwise, PiFile } from "react-icons/pi";

export default function UploaderWrapper({ onSuccess, setDataError, ...props }: UploaderWrapperProps) {
const [loading, setLoading] = useState(false);
const theme = useThemeStore((state) => state.theme);
const { t } = useTranslation();

const { getRootProps, getInputProps, isDragActive, open } = useDropzone({
noClick: true,
Expand All @@ -34,44 +36,42 @@ export default function UploaderWrapper({ onSuccess, setDataError, ...props }: U
});

return (
<Box
padding="15px"
border="1px solid var(--color-border)"
borderRadius="var(--border-radius-2)"
>
<Box
{...getRootProps()}
width="100%"
<Box padding="15px" border="1px solid var(--color-border)" borderRadius="var(--border-radius-2)">
<Box
{...getRootProps()}
width="100%"
height="100%"
display="flex"
justifyContent="center"
alignItems="center"
flexDirection="column"
display="flex"
justifyContent="center"
alignItems="center"
flexDirection="column"
flex={1}
border="2px dashed var(--color-border)"
borderRadius="var(--border-radius-2)"
>
<input {...getInputProps()}/>
borderRadius="var(--border-radius-2)">
<input {...getInputProps()} />
{isDragActive ? (
<Text>Drop your file here</Text>
<Text>{t("Drop your file here")}</Text>
) : loading ? (
<Text>Loading...</Text>
<Text>{t("Loading...")}</Text>
) : (
<>
<Text>Drop your file here</Text>
<Text>or</Text>
<Button
leftIcon={<PiFile />}
onClick={open}
mt="6px"
colorScheme={"secondary"}
<Text>{t("Drop your file here")}</Text>
<Text>{t("or")}</Text>
<Button
leftIcon={<PiFile />}
onClick={open}
mt="6px"
colorScheme={"secondary"}
variant={theme === "light" ? "outline" : "solid"}
_hover={theme === "light" ? {
background: "var(--color-border)",
color: "var(--color-text)"
} : undefined}
>
Browse files
_hover={
theme === "light"
? {
background: "var(--color-border)",
color: "var(--color-text)",
}
: undefined
}>
{t("Browse files")}
</Button>
</>
)}
Expand Down
8 changes: 5 additions & 3 deletions src/importer/features/complete/index.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
import { useTranslation } from "react-i18next";
import { Button } from "@chakra-ui/button";
import Box from "../../components/Box";
import { CompleteProps } from "./types";
import style from "./style/Complete.module.scss";
import { PiArrowCounterClockwise, PiCheckBold } from "react-icons/pi";

export default function Complete({ reload, close, isModal }: CompleteProps) {
const { t } = useTranslation();
return (
<Box className={style.content}>
<>
<span className={style.icon}>
<PiCheckBold />
</span>
<div>Import Successful</div>
<div>{t("Import Successful")}</div>
<div className={style.actions}>
<Button type="button" colorScheme="secondary" leftIcon={<PiArrowCounterClockwise />} onClick={reload}>
Upload another file
{t("Upload another file")}
</Button>
{isModal && (
<Button type="button" colorScheme="primary" leftIcon={<PiCheckBold />} onClick={close}>
Done
{t("Done")}
</Button>
)}
</div>
Expand Down
8 changes: 7 additions & 1 deletion src/importer/features/main/hooks/useStepNavigation.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import useStepper from "../../../components/Stepper/hooks/useStepper";
import { Steps } from "../types";
import useMutableLocalStorage from "./useMutableLocalStorage";
Expand Down Expand Up @@ -34,7 +35,12 @@ const getStepConfig = (skipHeader: boolean) => {
};

function useStepNavigation(initialStep: number, skipHeader: boolean) {
const stepper = useStepper(getStepConfig(skipHeader), StepEnum.Upload, skipHeader);
const [t] = useTranslation();
const translatedSteps = getStepConfig(skipHeader).map((step) => ({
...step,
label: t(step.label),
}));
const stepper = useStepper(translatedSteps, StepEnum.Upload, skipHeader);
const [storageStep, setStorageStep] = useMutableLocalStorage(`tf_steps`, "");
const [currentStep, setCurrentStep] = useState(initialStep);

Expand Down
5 changes: 4 additions & 1 deletion src/importer/features/main/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import MapColumns from "../map-columns";
import RowSelection from "../row-selection";
import Uploader from "../uploader";
import { PiX } from "react-icons/pi";
import { useTranslation } from "react-i18next";

export default function Main(props: CSVImporterProps) {
const {
Expand All @@ -31,6 +32,8 @@ export default function Main(props: CSVImporterProps) {
} = props;
const skipHeader = skipHeaderRowSelection ?? false;

const { t } = useTranslation();

// Apply custom styles
useCustomStyles(parseObjectOrStringJSON("customStyles", customStyles));

Expand Down Expand Up @@ -120,7 +123,7 @@ export default function Main(props: CSVImporterProps) {
setDataError(null);
const fileType = file.name.slice(file.name.lastIndexOf(".") + 1);
if (!["csv", "xls", "xlsx"].includes(fileType)) {
setDataError("Only CSV, XLS, and XLSX files can be uploaded");
setDataError(t("Only CSV, XLS, and XLSX files can be uploaded"));
return;
}
const reader = new FileReader();
Expand Down
Loading