Add i18n and some languages

This commit is contained in:
Nicola Benaglia 2025-04-21 12:14:51 +02:00
parent 655c990917
commit d2a82519ad
10 changed files with 113 additions and 9 deletions

View File

@ -21,4 +21,5 @@ Many additional details and a fully featured wiki will be created over time. Rea
## Internationalization (i18n) ## Internationalization (i18n)
Qortal-Hub supports internationalization (i18n) using [i18next](https://www.i18next.com/), allowing seamless translation of UI text into multiple languages. Qortal-Hub supports internationalization (i18n) using [i18next](https://www.i18next.com/), allowing seamless translation of UI text into multiple languages.
The setup includes modularized translation files, language detection, and runtime language switching. The setup includes modularized translation files, language detection, context and runtime language switching.
Files with translation are in `public/locales/<locale>` folder.

48
i18n.js Normal file
View File

@ -0,0 +1,48 @@
import { initReactI18next } from 'react-i18next';
import HttpBackend from 'i18next-http-backend';
import LocalStorageBackend from 'i18next-localstorage-backend';
import HttpApi from 'i18next-http-backend';
import i18n from 'i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
// Detect environment
const isDev = process.env.NODE_ENV === 'development';
// Register custom postProcessor: it capitalizes the first letter of a translation-
// Usage:
// t('greeting', { postProcess: 'capitalize' })
const capitalize = {
type: 'postProcessor',
name: 'capitalize',
process: (value, key, options, translator) => {
return value.charAt(0).toUpperCase() + value.slice(1);
},
};
i18n
.use(HttpApi)
.use(LanguageDetector)
.use(initReactI18next)
.use(capitalize)
.init({
debug: isDev,
fallbackLng: 'en',
ns: ['auth', 'core'],
supportedLngs: ['en', 'it', 'fr', 'es'],
backend: {
backends: [LocalStorageBackend, HttpBackend],
backendOptions: [
{
expirationTime: 7 * 24 * 60 * 60 * 1000, // 7 days
},
{
loadPath: '/locales/{{lng}}/{{ns}}.json',
},
],
},
interpolation: {
escapeValue: false,
},
});
export default i18n;

10
package-lock.json generated
View File

@ -59,6 +59,7 @@
"i18next": "^25.0.1", "i18next": "^25.0.1",
"i18next-browser-languagedetector": "^8.0.5", "i18next-browser-languagedetector": "^8.0.5",
"i18next-http-backend": "^3.0.2", "i18next-http-backend": "^3.0.2",
"i18next-localstorage-backend": "^4.2.0",
"jssha": "3.3.1", "jssha": "3.3.1",
"lit": "^3.2.1", "lit": "^3.2.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
@ -10846,6 +10847,15 @@
"cross-fetch": "4.0.0" "cross-fetch": "4.0.0"
} }
}, },
"node_modules/i18next-localstorage-backend": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/i18next-localstorage-backend/-/i18next-localstorage-backend-4.2.0.tgz",
"integrity": "sha512-vglEQF0AnLriX7dLA2drHnqAYzHxnLwWQzBDw8YxcIDjOvYZz5rvpal59Dq4In+IHNmGNM32YgF0TDjBT0fHmA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.22.15"
}
},
"node_modules/iconv-corefoundation": { "node_modules/iconv-corefoundation": {
"version": "1.1.7", "version": "1.1.7",
"resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz",

View File

@ -64,6 +64,7 @@
"i18next": "^25.0.1", "i18next": "^25.0.1",
"i18next-browser-languagedetector": "^8.0.5", "i18next-browser-languagedetector": "^8.0.5",
"i18next-http-backend": "^3.0.2", "i18next-http-backend": "^3.0.2",
"i18next-localstorage-backend": "^4.2.0",
"jssha": "3.3.1", "jssha": "3.3.1",
"lit": "^3.2.1", "lit": "^3.2.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",

View File

@ -0,0 +1,10 @@
{
"account_many": "accounts",
"account_one": "account",
"advanced_users": "for advanced users",
"create_account": "create account",
"welcome": "Welcome to",
"use_local_node": "use local node",
"change_apikey": "change API key",
"choose_custom_node": "choose custom node"
}

View File

@ -0,0 +1,9 @@
{
"add": {
"task": "Add task"
},
"cancel": "Cancel",
"description": "Description" ,
"save": "Save",
"title": "Title"
}

View File

@ -0,0 +1,10 @@
{
"account_many": "account",
"account_one": "account",
"advanced_users": "Per utenti avanzati",
"change_apikey": "Cambia la chiave API",
"choose_custom_node": "Scegli un nodo custom",
"create_account": "crea un account",
"use_local_node": "Usa nodo locale",
"welcome": "Benvenuto in"
}

View File

@ -0,0 +1,9 @@
{
"add": {
"task": "Aggiungi compito"
},
"cancel": "Cancella",
"description": "Descrizione" ,
"save": "Salva",
"title": "Titolo"
}

View File

@ -30,6 +30,7 @@ import { cleanUrl, gateways } from '../background';
import { GlobalContext } from '../App'; import { GlobalContext } from '../App';
import Tooltip, { TooltipProps, tooltipClasses } from '@mui/material/Tooltip'; import Tooltip, { TooltipProps, tooltipClasses } from '@mui/material/Tooltip';
import ThemeSelector from '../components/Theme/ThemeSelector'; import ThemeSelector from '../components/Theme/ThemeSelector';
import { useTranslation } from 'react-i18next';
const manifestData = { const manifestData = {
version: '0.5.3', version: '0.5.3',
@ -84,6 +85,7 @@ export const NotAuthenticated = ({
React.useState(null); React.useState(null);
const { showTutorial, hasSeenGettingStarted } = useContext(GlobalContext); const { showTutorial, hasSeenGettingStarted } = useContext(GlobalContext);
const theme = useTheme(); const theme = useTheme();
const { t } = useTranslation('auth');
const importedApiKeyRef = useRef(null); const importedApiKeyRef = useRef(null);
const currentNodeRef = useRef(null); const currentNodeRef = useRef(null);
@ -183,6 +185,7 @@ export const NotAuthenticated = ({
useEffect(() => { useEffect(() => {
importedApiKeyRef.current = importedApiKey; importedApiKeyRef.current = importedApiKey;
}, [importedApiKey]); }, [importedApiKey]);
useEffect(() => { useEffect(() => {
currentNodeRef.current = currentNode; currentNodeRef.current = currentNode;
}, [currentNode]); }, [currentNode]);
@ -309,6 +312,7 @@ export const NotAuthenticated = ({
} else if (currentNodeRef.current) { } else if (currentNodeRef.current) {
payload = currentNodeRef.current; payload = currentNodeRef.current;
} }
let isValid = false; let isValid = false;
const url = `${payload?.url}/admin/settings/localAuthBypassEnabled`; const url = `${payload?.url}/admin/settings/localAuthBypassEnabled`;
@ -402,6 +406,7 @@ export const NotAuthenticated = ({
const addCustomNode = () => { const addCustomNode = () => {
setMode('add-node'); setMode('add-node');
}; };
const saveCustomNodes = (myNodes, isFullListOfNodes) => { const saveCustomNodes = (myNodes, isFullListOfNodes) => {
let nodes = [...(myNodes || [])]; let nodes = [...(myNodes || [])];
if (!isFullListOfNodes && customNodeToSaveIndex !== null) { if (!isFullListOfNodes && customNodeToSaveIndex !== null) {
@ -455,7 +460,9 @@ export const NotAuthenticated = ({
> >
<img src={Logo1Dark} className="base-image" /> <img src={Logo1Dark} className="base-image" />
</div> </div>
<Spacer height="30px" /> <Spacer height="30px" />
<TextP <TextP
sx={{ sx={{
textAlign: 'center', textAlign: 'center',
@ -463,7 +470,7 @@ export const NotAuthenticated = ({
fontSize: '18px', fontSize: '18px',
}} }}
> >
WELCOME TO {t('auth:welcome', { postProcess: 'capitalize' })}
<TextSpan <TextSpan
sx={{ sx={{
fontSize: '18px', fontSize: '18px',
@ -504,13 +511,9 @@ export const NotAuthenticated = ({
} }
> >
<CustomButton onClick={() => setExtstate('wallets')}> <CustomButton onClick={() => setExtstate('wallets')}>
{/* <input {...getInputProps()} /> */} {t('auth:account_many', { postProcess: 'capitalize' })}
Accounts
</CustomButton> </CustomButton>
</HtmlTooltip> </HtmlTooltip>
{/* <Tooltip title="Authenticate by importing your Qortal JSON file" arrow>
<img src={Info} />
</Tooltip> */}
</Box> </Box>
<Spacer height="6px" /> <Spacer height="6px" />
@ -565,10 +568,11 @@ export const NotAuthenticated = ({
}, },
}} }}
> >
Create account {t('auth:create_account', { postProcess: 'capitalize' })}
</CustomButton> </CustomButton>
</HtmlTooltip> </HtmlTooltip>
</Box> </Box>
<Spacer height="15px" /> <Spacer height="15px" />
<Typography <Typography
@ -579,6 +583,7 @@ export const NotAuthenticated = ({
> >
{'Using node: '} {currentNode?.url} {'Using node: '} {currentNode?.url}
</Typography> </Typography>
<> <>
<Spacer height="15px" /> <Spacer height="15px" />
<Box <Box
@ -603,7 +608,7 @@ export const NotAuthenticated = ({
textDecoration: 'underline', textDecoration: 'underline',
}} }}
> >
For advanced users {t('auth:advanced_users', { postProcess: 'capitalize' })}
</Typography> </Typography>
<Box <Box
sx={{ sx={{

View File

@ -6,6 +6,7 @@ import { MessageQueueProvider } from './MessageQueueContext.tsx';
import { RecoilRoot } from 'recoil'; import { RecoilRoot } from 'recoil';
import { ThemeProvider } from './components/Theme/ThemeContext.tsx'; import { ThemeProvider } from './components/Theme/ThemeContext.tsx';
import { CssBaseline } from '@mui/material'; import { CssBaseline } from '@mui/material';
import '../i18n';
ReactDOM.createRoot(document.getElementById('root')!).render( ReactDOM.createRoot(document.getElementById('root')!).render(
<> <>