[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:
Oliver 2024-01-17 16:29:06 +11:00 committed by GitHub
parent 386aa5952c
commit 7d36049ac9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
43 changed files with 129 additions and 91 deletions

1
.gitignore vendored
View File

@ -105,6 +105,7 @@ InvenTree/plugins/
# Compiled translation files
*.mo
messages.ts
# web frontend (static files)
InvenTree/web/static

View File

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

View 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)')),
]

View File

@ -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}')

View File

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

View File

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

View File

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

View 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

View File

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

View File

@ -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({
status: build.status,
type: ModelType.build
});
return build?.status ? (
StatusRenderer({
status: build.status,
type: ModelType.build
})
) : (
<Skeleton />
);
}, [build, id]);
return (

View File

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

View File

@ -298,7 +298,7 @@ def static(c, frontend=False):
manage(c, 'prerender')
if frontend and node_available():
frontend_build(c)
manage(c, 'collectstatic --no-input')
manage(c, 'collectstatic --no-input --clear')
@task