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)
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-browser-languagedetector": "^8.0.5",
"i18next-http-backend": "^3.0.2",
"i18next-localstorage-backend": "^4.2.0",
"jssha": "3.3.1",
"lit": "^3.2.1",
"lodash": "^4.17.21",
@ -10846,6 +10847,15 @@
"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": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz",

View File

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

View File

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