mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
[React] UI Translation Updates (#6257)
* Move locales definition into separate file - Cleanup settings.py a bit * Update docstring * Expose 'default_locale' to info API endpoint * Validate settings.LANGUAGE_CODE * Fix bug in BuildDetail page * Use selected language when making API queries * Translate more strings * Tweak variable name * Update locale config * Remove duplicate code * Remove compiled messages.ts translation files * Fixes for LanguageContext.tsx * Update messages.d.ts for sr locale * Ensure compiled files are served by django runserver * Amend changes to STATICFILES_DIRS * Cleanup prerender.py * Refetch status codes when locale is changed * Fix log msg * Clear out old static files
This commit is contained in:
parent
386aa5952c
commit
7d36049ac9
1
.gitignore
vendored
1
.gitignore
vendored
@ -105,6 +105,7 @@ InvenTree/plugins/
|
||||
|
||||
# Compiled translation files
|
||||
*.mo
|
||||
messages.ts
|
||||
|
||||
# web frontend (static files)
|
||||
InvenTree/web/static
|
||||
|
@ -125,6 +125,7 @@ class InfoView(AjaxView):
|
||||
'platform': InvenTree.version.inventreePlatform() if is_staff else None,
|
||||
'installer': InvenTree.version.inventreeInstaller() if is_staff else None,
|
||||
'target': InvenTree.version.inventreeTarget() if is_staff else None,
|
||||
'default_locale': settings.LANGUAGE_CODE,
|
||||
}
|
||||
|
||||
return JsonResponse(data)
|
||||
|
47
InvenTree/InvenTree/locales.py
Normal file
47
InvenTree/InvenTree/locales.py
Normal file
@ -0,0 +1,47 @@
|
||||
"""Support translation locales for InvenTree.
|
||||
|
||||
If a new language translation is supported, it must be added here
|
||||
After adding a new language, run the following command:
|
||||
python manage.py makemessages -l <language_code> -e html,js,py --no-wrap
|
||||
where <language_code> is the code for the new language
|
||||
Additionally, update the following files with the new locale code:
|
||||
|
||||
- /src/frontend/.linguirc file
|
||||
- /src/frontend/src/context/LanguageContext.tsx
|
||||
"""
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
LOCALES = [
|
||||
('bg', _('Bulgarian')),
|
||||
('cs', _('Czech')),
|
||||
('da', _('Danish')),
|
||||
('de', _('German')),
|
||||
('el', _('Greek')),
|
||||
('en', _('English')),
|
||||
('es', _('Spanish')),
|
||||
('es-mx', _('Spanish (Mexican)')),
|
||||
('fa', _('Farsi / Persian')),
|
||||
('fi', _('Finnish')),
|
||||
('fr', _('French')),
|
||||
('he', _('Hebrew')),
|
||||
('hi', _('Hindi')),
|
||||
('hu', _('Hungarian')),
|
||||
('it', _('Italian')),
|
||||
('ja', _('Japanese')),
|
||||
('ko', _('Korean')),
|
||||
('nl', _('Dutch')),
|
||||
('no', _('Norwegian')),
|
||||
('pl', _('Polish')),
|
||||
('pt', _('Portuguese')),
|
||||
('pt-br', _('Portuguese (Brazilian)')),
|
||||
('ru', _('Russian')),
|
||||
('sl', _('Slovenian')),
|
||||
('sr', _('Serbian')),
|
||||
('sv', _('Swedish')),
|
||||
('th', _('Thai')),
|
||||
('tr', _('Turkish')),
|
||||
('vi', _('Vietnamese')),
|
||||
('zh-hans', _('Chinese (Simplified)')),
|
||||
('zh-hant', _('Chinese (Traditional)')),
|
||||
]
|
@ -58,10 +58,9 @@ class Command(BaseCommand):
|
||||
for file in os.listdir(SOURCE_DIR):
|
||||
path = os.path.join(SOURCE_DIR, file)
|
||||
if os.path.exists(path) and os.path.isfile(path):
|
||||
print(f'render {file}')
|
||||
render_file(file, SOURCE_DIR, TARGET_DIR, locales, ctx)
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
'Using multi-level directories is not implemented at this point'
|
||||
) # TODO multilevel dir if needed
|
||||
print(f'rendered all files in {SOURCE_DIR}')
|
||||
print(f'Rendered all files in {SOURCE_DIR}')
|
||||
|
@ -28,7 +28,7 @@ from InvenTree.config import get_boolean_setting, get_custom_file, get_setting
|
||||
from InvenTree.sentry import default_sentry_dsn, init_sentry
|
||||
from InvenTree.version import checkMinPythonVersion, inventreeApiVersion
|
||||
|
||||
from . import config
|
||||
from . import config, locales
|
||||
|
||||
checkMinPythonVersion()
|
||||
|
||||
@ -160,6 +160,10 @@ STATICFILES_I18_TRG = BASE_DIR.joinpath('InvenTree', 'static_i18n')
|
||||
STATICFILES_DIRS.append(STATICFILES_I18_TRG)
|
||||
STATICFILES_I18_TRG = STATICFILES_I18_TRG.joinpath(STATICFILES_I18_PREFIX)
|
||||
|
||||
# Append directory for compiled react files if debug server is running
|
||||
if DEBUG and 'collectstatic' not in sys.argv:
|
||||
STATICFILES_DIRS.append(BASE_DIR.joinpath('web', 'static'))
|
||||
|
||||
STATFILES_I18_PROCESSORS = ['InvenTree.context.status_codes']
|
||||
|
||||
# Color Themes Directory
|
||||
@ -822,52 +826,26 @@ if type(EXTRA_URL_SCHEMES) not in [list]: # pragma: no cover
|
||||
logger.warning('extra_url_schemes not correctly formatted')
|
||||
EXTRA_URL_SCHEMES = []
|
||||
|
||||
LANGUAGES = locales.LOCALES
|
||||
|
||||
LOCALE_CODES = [lang[0] for lang in LANGUAGES]
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/dev/topics/i18n/
|
||||
LANGUAGE_CODE = get_setting('INVENTREE_LANGUAGE', 'language', 'en-us')
|
||||
|
||||
if (
|
||||
LANGUAGE_CODE not in LOCALE_CODES
|
||||
and LANGUAGE_CODE.split('-')[0] not in LOCALE_CODES
|
||||
): # pragma: no cover
|
||||
logger.warning(
|
||||
'Language code %s not supported - defaulting to en-us', LANGUAGE_CODE
|
||||
)
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
|
||||
# Store language settings for 30 days
|
||||
LANGUAGE_COOKIE_AGE = 2592000
|
||||
|
||||
# If a new language translation is supported, it must be added here
|
||||
# After adding a new language, run the following command:
|
||||
# python manage.py makemessages -l <language_code> -e html,js,py --no-wrap
|
||||
# where <language_code> is the code for the new language
|
||||
# Additionally, update the /src/frontend/.linguirc file
|
||||
LANGUAGES = [
|
||||
('bg', _('Bulgarian')),
|
||||
('cs', _('Czech')),
|
||||
('da', _('Danish')),
|
||||
('de', _('German')),
|
||||
('el', _('Greek')),
|
||||
('en', _('English')),
|
||||
('es', _('Spanish')),
|
||||
('es-mx', _('Spanish (Mexican)')),
|
||||
('fa', _('Farsi / Persian')),
|
||||
('fi', _('Finnish')),
|
||||
('fr', _('French')),
|
||||
('he', _('Hebrew')),
|
||||
('hi', _('Hindi')),
|
||||
('hu', _('Hungarian')),
|
||||
('it', _('Italian')),
|
||||
('ja', _('Japanese')),
|
||||
('ko', _('Korean')),
|
||||
('nl', _('Dutch')),
|
||||
('no', _('Norwegian')),
|
||||
('pl', _('Polish')),
|
||||
('pt', _('Portuguese')),
|
||||
('pt-br', _('Portuguese (Brazilian)')),
|
||||
('ru', _('Russian')),
|
||||
('sl', _('Slovenian')),
|
||||
('sr', _('Serbian')),
|
||||
('sv', _('Swedish')),
|
||||
('th', _('Thai')),
|
||||
('tr', _('Turkish')),
|
||||
('vi', _('Vietnamese')),
|
||||
('zh-hans', _('Chinese (Simplified)')),
|
||||
('zh-hant', _('Chinese (Traditional)')),
|
||||
]
|
||||
|
||||
|
||||
# Testing interface translations
|
||||
if get_boolean_setting('TEST_TRANSLATIONS', default_value=False): # pragma: no cover
|
||||
# Set default language
|
||||
|
@ -5,11 +5,14 @@ import { LoadingOverlay, Text } from '@mantine/core';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { api } from '../App';
|
||||
import { useServerApiState } from '../states/ApiState';
|
||||
import { useLocalState } from '../states/LocalState';
|
||||
|
||||
// Definitions
|
||||
export type Locales = keyof typeof languages | 'pseudo-LOCALE';
|
||||
|
||||
export const defaultLocale = 'en';
|
||||
|
||||
export const languages: Record<string, string> = {
|
||||
bg: t`Bulgarian`,
|
||||
cs: t`Czech`,
|
||||
@ -45,6 +48,11 @@ export const languages: Record<string, string> = {
|
||||
|
||||
export function LanguageContext({ children }: { children: JSX.Element }) {
|
||||
const [language] = useLocalState((state) => [state.language]);
|
||||
const [server] = useServerApiState((state) => [state.server]);
|
||||
|
||||
useEffect(() => {
|
||||
activateLocale(defaultLocale);
|
||||
}, []);
|
||||
|
||||
const [loadedState, setLoadedState] = useState<
|
||||
'loading' | 'loaded' | 'error'
|
||||
@ -57,6 +65,32 @@ export function LanguageContext({ children }: { children: JSX.Element }) {
|
||||
activateLocale(language)
|
||||
.then(() => {
|
||||
if (isMounted.current) setLoadedState('loaded');
|
||||
|
||||
/*
|
||||
* Configure the default Accept-Language header for all requests.
|
||||
* - Locally selected locale
|
||||
* - Server default locale
|
||||
* - en-us (backup)
|
||||
*/
|
||||
let locales: (string | undefined)[] = [];
|
||||
|
||||
if (language != 'pseudo-LOCALE') {
|
||||
locales.push(language);
|
||||
}
|
||||
|
||||
if (!!server.default_locale) {
|
||||
locales.push(server.default_locale);
|
||||
}
|
||||
|
||||
if (locales.indexOf('en-us') < 0) {
|
||||
locales.push('en-us');
|
||||
}
|
||||
|
||||
// Update default Accept-Language headers
|
||||
api.defaults.headers.common['Accept-Language'] = locales.join(', ');
|
||||
|
||||
// Reload server state (refresh status codes)
|
||||
useServerApiState.getState().fetchServerApiState();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Failed loading translations', err);
|
||||
@ -90,7 +124,4 @@ export async function activateLocale(locale: Locales) {
|
||||
const { messages } = await import(`../locales/${locale}/messages.ts`);
|
||||
i18n.load(locale, messages);
|
||||
i18n.activate(locale);
|
||||
|
||||
// Set api header
|
||||
api.defaults.headers.common['Accept-Language'] = locale;
|
||||
}
|
||||
|
@ -16,7 +16,8 @@ export const emptyServerAPI = {
|
||||
system_health: null,
|
||||
platform: null,
|
||||
installer: null,
|
||||
target: null
|
||||
target: null,
|
||||
default_locale: null
|
||||
};
|
||||
|
||||
export interface SiteMarkProps {
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
4
src/frontend/src/locales/sr/messages.d.ts
vendored
Normal file
4
src/frontend/src/locales/sr/messages.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
import { Messages } from '@lingui/core';
|
||||
declare const messages: Messages;
|
||||
export { messages };
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,4 +1,4 @@
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { Trans, t } from '@lingui/macro';
|
||||
import { Button, Group, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { useToggle } from '@mantine/hooks';
|
||||
@ -37,13 +37,13 @@ export function AccountDetailPanel() {
|
||||
{editing ? (
|
||||
<Stack spacing="xs">
|
||||
<TextInput
|
||||
label="First name"
|
||||
placeholder="First name"
|
||||
label="first name"
|
||||
placeholder={t`First name`}
|
||||
{...form.getInputProps('first_name')}
|
||||
/>
|
||||
<TextInput
|
||||
label="Last name"
|
||||
placeholder="Last name"
|
||||
placeholder={t`Last name`}
|
||||
{...form.getInputProps('last_name')}
|
||||
/>
|
||||
<Group position="right" mt="md">
|
||||
@ -55,10 +55,12 @@ export function AccountDetailPanel() {
|
||||
) : (
|
||||
<Stack spacing="0">
|
||||
<Text>
|
||||
<Trans>First name: {form.values.first_name}</Trans>
|
||||
<Trans>First name: </Trans>
|
||||
{form.values.first_name}
|
||||
</Text>
|
||||
<Text>
|
||||
<Trans>Last name: {form.values.last_name}</Trans>
|
||||
<Trans>Last name: </Trans>
|
||||
{form.values.last_name}
|
||||
</Text>
|
||||
</Stack>
|
||||
)}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { t } from '@lingui/macro';
|
||||
import { Group, LoadingOverlay, Stack, Table } from '@mantine/core';
|
||||
import { Group, LoadingOverlay, Skeleton, Stack, Table } from '@mantine/core';
|
||||
import {
|
||||
IconClipboardCheck,
|
||||
IconClipboardList,
|
||||
@ -78,7 +78,7 @@ export default function BuildDetail() {
|
||||
<tr>
|
||||
<td>{t`Build Status`}</td>
|
||||
<td>
|
||||
{build.status && (
|
||||
{build?.status && (
|
||||
<StatusRenderer
|
||||
status={build.status}
|
||||
type={ModelType.build}
|
||||
@ -241,10 +241,14 @@ export default function BuildDetail() {
|
||||
}, [id, build, user]);
|
||||
|
||||
const buildDetail = useMemo(() => {
|
||||
return StatusRenderer({
|
||||
return build?.status ? (
|
||||
StatusRenderer({
|
||||
status: build.status,
|
||||
type: ModelType.build
|
||||
});
|
||||
})
|
||||
) : (
|
||||
<Skeleton />
|
||||
);
|
||||
}, [build, id]);
|
||||
|
||||
return (
|
||||
|
@ -37,6 +37,7 @@ export interface ServerAPIProps {
|
||||
platform: null | string;
|
||||
installer: null | string;
|
||||
target: null | string;
|
||||
default_locale: null | string;
|
||||
}
|
||||
|
||||
// Type interface defining a single 'setting' object
|
||||
|
Loading…
Reference in New Issue
Block a user