1
0
mirror of https://github.com/Qortal/Qortal-Hub.git synced 2025-08-06 12:31:23 +00:00

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

@@ -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

@@ -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

@@ -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",

@@ -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",

@@ -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"
}

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

@@ -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"
}

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

@@ -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={{

@@ -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(
<> <>