feat(i18n): Add ability to translate the entire page including momentjs and Ant Design (#50)
All checks were successful
/ build (push) Successful in 5m22s
All checks were successful
/ build (push) Successful in 5m22s
Co-authored-by: Varakh <varakh@varakh.de> Co-committed-by: Varakh <varakh@varakh.de>
This commit is contained in:
parent
ac1ae19e33
commit
5c089989cf
17 changed files with 331 additions and 29 deletions
|
@ -30,6 +30,74 @@ Use the `npm run start` command to start the development setup. Backend should b
|
|||
|
||||
Pipeline checks for existing i18n keys, if you need to manually sync any language file, run `npm run i18n-sync`.
|
||||
|
||||
#### New translation languages
|
||||
|
||||
Adapt `package.json` and add the respective key to `i18n-sync` and `checkstyle:i18n` for the `--languages` argument,
|
||||
e.g. `--languages en,new`.
|
||||
|
||||
Run `npm run i18n-sync` to create and sync new language
|
||||
|
||||
Adapt `languages.ts`
|
||||
|
||||
```typescript
|
||||
enum Languages {
|
||||
// ...
|
||||
NEW = 'new'
|
||||
}
|
||||
```
|
||||
|
||||
Adapt `locales.ts`
|
||||
|
||||
```typescript
|
||||
enum Locales {
|
||||
// ...
|
||||
NEW = 'new-NEW'
|
||||
}
|
||||
```
|
||||
|
||||
Adapt `LocaleProvider.tsx` to detect the new language, assign proper _momentjs_ locale.
|
||||
|
||||
```typescript
|
||||
// add import for Ant Design
|
||||
import new_NEW from 'antd/lib/locale/new_NEW';
|
||||
|
||||
// add import for moment
|
||||
// @ts-ignore
|
||||
import new_localization from 'moment/dist/locale/new.js';
|
||||
|
||||
// getLocaleFromLanguage
|
||||
if (LANGUAGES.NEW === language) {
|
||||
// ...
|
||||
}
|
||||
|
||||
// getLanguageFromLocale
|
||||
if (LANGUAGES.NEW === language) {
|
||||
// ...
|
||||
}
|
||||
|
||||
// setLocales (uses import of moment)
|
||||
// ...
|
||||
else if (Languages.NEW === language) {
|
||||
// use import for Ant Design
|
||||
setAntLocale(new_NEW);
|
||||
// use import for moment
|
||||
moment.updateLocale(language, de_localization);
|
||||
}
|
||||
```
|
||||
|
||||
Adapt `i18n/index.ts` and import new language and make i18n detect it (locales are derived from it)
|
||||
|
||||
```typescript jsx
|
||||
import newTranslation from './translations/new.json';
|
||||
|
||||
const resources = {
|
||||
//...
|
||||
new: {
|
||||
...newTranslation
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### Dependencies
|
||||
|
||||
To track unused dependencies
|
||||
|
|
|
@ -26,8 +26,8 @@
|
|||
"checkstyle:format": "npm run format:check",
|
||||
"checkstyle:ts": "eslint \"./src/**/*.{ts,tsx}\" -f checkstyle > ci/eslint.xml",
|
||||
"checkstyle:less": "stylelint \"./src/**/*.less\"",
|
||||
"checkstyle:i18n": "sync-i18n --check --files '**/translations/*.json' --primary en --space 2 --lineendings LF",
|
||||
"i18n-sync": "sync-i18n --files '**/translations/*.json' --primary en --space 2 --lineendings LF",
|
||||
"checkstyle:i18n": "sync-i18n --check --files '**/translations/*.json' --primary en --languages en --space 2 --lineendings LF",
|
||||
"i18n-sync": "sync-i18n --files '**/translations/*.json' --primary en --languages en --space 2 --lineendings LF",
|
||||
"clean": "npx --quiet rimraf build && npx --quiet rimraf node_modules"
|
||||
},
|
||||
"dependencies": {
|
||||
|
|
8
server/web/src/constants/dateTimeStyle.ts
Normal file
8
server/web/src/constants/dateTimeStyle.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
enum DateTimeStyle {
|
||||
FULL = 'full',
|
||||
LONG = 'long',
|
||||
MEDIUM = 'medium',
|
||||
SHORT = 'short'
|
||||
}
|
||||
|
||||
export default DateTimeStyle;
|
6
server/web/src/constants/languages.ts
Normal file
6
server/web/src/constants/languages.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
enum Languages {
|
||||
ENGLISH = 'en',
|
||||
DEFAULT = ENGLISH
|
||||
}
|
||||
|
||||
export default Languages;
|
6
server/web/src/constants/locales.ts
Normal file
6
server/web/src/constants/locales.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
enum Locales {
|
||||
ENGLISH_US = 'en-US',
|
||||
DEFAULT = ENGLISH_US
|
||||
}
|
||||
|
||||
export default Locales;
|
|
@ -17,7 +17,6 @@ i18next
|
|||
.init(
|
||||
{
|
||||
resources,
|
||||
lng: 'en',
|
||||
fallbackLng: 'en',
|
||||
debug: isDevelopment(),
|
||||
interpolation: {
|
||||
|
|
|
@ -209,6 +209,7 @@
|
|||
"back_home": "Go back",
|
||||
"events": "Events",
|
||||
"home": "Home",
|
||||
"loading": "Loading...",
|
||||
"login": "Login",
|
||||
"logout": "Logout",
|
||||
"secrets": "Secrets",
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import './i18n';
|
||||
import App from './App';
|
||||
import LocaleContextProvider from './providers/LocaleContextProvider';
|
||||
import store from './store';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { Provider } from 'react-redux';
|
||||
|
@ -15,7 +16,9 @@ root.render(
|
|||
* Fix not yet available. follow thread: https://github.com/ant-design/ant-design/issues/22493 */}
|
||||
{/*<React.StrictMode>*/}
|
||||
<Router>
|
||||
<App />
|
||||
<LocaleContextProvider>
|
||||
<App />
|
||||
</LocaleContextProvider>
|
||||
</Router>
|
||||
{/*</React.StrictMode>*/}
|
||||
</Provider>
|
||||
|
|
|
@ -2,7 +2,9 @@ import ItemActionInvocation from './ItemActionInvocation';
|
|||
import { useGetActionInvocationsQuery } from '../../api/actionInvocationsApi';
|
||||
import ActionInvocationFilterQueryParamNames from '../../constants/api/actionInvocationFilterQueryParamNames';
|
||||
import ActionInvocationOrder from '../../constants/api/actionInvocationOrder';
|
||||
import DateTimeStyle from '../../constants/dateTimeStyle';
|
||||
import { PAGE_DEFAULT, PAGE_DEFAULT_OPTIONS, PAGE_SIZE_DEFAULT } from '../../constants/pagination';
|
||||
import { useLocaleProviderContext } from '../../providers/LocaleContextProvider';
|
||||
import { ActionInvocationResponse, ActionInvocationsRequestParams, ActionInvocationState } from '../../types';
|
||||
import useActionInvocationsFilterQueryParams from '../../use/useActionInvocationsFilterQueryParams';
|
||||
import { convertToLowerCaseUnderscore } from '../../utils/apiHelper';
|
||||
|
@ -30,6 +32,7 @@ const DEFAULT_POLLING_INTERVAL = 5000;
|
|||
|
||||
const ActionInvocationsPage = () => {
|
||||
const [t] = useTranslation('action_invocations');
|
||||
const { locale } = useLocaleProviderContext();
|
||||
const [pollingInterval, setPollingInterval] = useState<number>(DEFAULT_POLLING_INTERVAL);
|
||||
|
||||
const [queryParams, setSearchQueryParams] = useSearchParams();
|
||||
|
@ -172,7 +175,7 @@ const ActionInvocationsPage = () => {
|
|||
ellipsis: true,
|
||||
responsive: ['sm', 'md', 'lg', 'xl', 'xxl'],
|
||||
sorter: (a, b) => sortAlphaIgnoringCase(a.createdAt, b.createdAt),
|
||||
render: (value) => formatDateTimeWithTimeZone(value)
|
||||
render: (value) => formatDateTimeWithTimeZone(value, DateTimeStyle.LONG, DateTimeStyle.MEDIUM, locale)
|
||||
},
|
||||
{
|
||||
title: t('col_updated_at'),
|
||||
|
@ -181,10 +184,10 @@ const ActionInvocationsPage = () => {
|
|||
ellipsis: true,
|
||||
responsive: ['sm', 'md', 'lg', 'xl', 'xxl'],
|
||||
sorter: (a, b) => sortAlphaIgnoringCase(a.updatedAt, b.updatedAt),
|
||||
render: (value) => formatDateTimeWithTimeZone(value)
|
||||
render: (value) => formatDateTimeWithTimeZone(value, DateTimeStyle.LONG, DateTimeStyle.MEDIUM, locale)
|
||||
}
|
||||
];
|
||||
}, [renderState, t]);
|
||||
}, [locale, renderState, t]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -6,7 +6,9 @@ import UpdateLabelAction from './UpdateLabelAction';
|
|||
import { useGetActionsQuery } from '../../api/actionsApi';
|
||||
import ActionFilterQueryParamNames from '../../constants/api/actionFilterQueryParamNames';
|
||||
import ActionOrder from '../../constants/api/actionOrder';
|
||||
import DateTimeStyle from '../../constants/dateTimeStyle';
|
||||
import { PAGE_DEFAULT, PAGE_DEFAULT_OPTIONS, PAGE_SIZE_DEFAULT } from '../../constants/pagination';
|
||||
import { useLocaleProviderContext } from '../../providers/LocaleContextProvider';
|
||||
import { ActionResponse, ActionsRequestParams, ActionType } from '../../types';
|
||||
import useActionsFilterQueryParams from '../../use/useActionsFilterQueryParams';
|
||||
import { convertToLowerCaseUnderscore } from '../../utils/apiHelper';
|
||||
|
@ -27,6 +29,7 @@ const DEFAULT_POLLING_INTERVAL = 5000;
|
|||
|
||||
const ActionsPage = () => {
|
||||
const [t] = useTranslation('actions');
|
||||
const { locale } = useLocaleProviderContext();
|
||||
const [pollingInterval, setPollingInterval] = useState<number>(0);
|
||||
|
||||
const [queryParams, setSearchQueryParams] = useSearchParams();
|
||||
|
@ -135,7 +138,7 @@ const ActionsPage = () => {
|
|||
ellipsis: true,
|
||||
responsive: ['sm', 'md', 'lg', 'xl', 'xxl'],
|
||||
sorter: (a, b) => sortAlphaIgnoringCase(a.createdAt, b.createdAt),
|
||||
render: (value) => formatDateTimeWithTimeZone(value)
|
||||
render: (value) => formatDateTimeWithTimeZone(value, DateTimeStyle.LONG, DateTimeStyle.MEDIUM, locale)
|
||||
},
|
||||
{
|
||||
title: t('col_updated_at'),
|
||||
|
@ -144,10 +147,10 @@ const ActionsPage = () => {
|
|||
ellipsis: true,
|
||||
responsive: ['sm', 'md', 'lg', 'xl', 'xxl'],
|
||||
sorter: (a, b) => sortAlphaIgnoringCase(a.updatedAt, b.updatedAt),
|
||||
render: (value) => formatDateTimeWithTimeZone(value)
|
||||
render: (value) => formatDateTimeWithTimeZone(value, DateTimeStyle.LONG, DateTimeStyle.MEDIUM, locale)
|
||||
}
|
||||
];
|
||||
}, [t]);
|
||||
}, [locale, t]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import EventText from './EventText';
|
||||
import { useDeleteEventMutation } from '../../api/eventsApi';
|
||||
import DateTimeStyle from '../../constants/dateTimeStyle';
|
||||
import { useLocaleProviderContext } from '../../providers/LocaleContextProvider';
|
||||
import { EventResponse } from '../../types/event';
|
||||
import { formatDateTimeWithTimeZone } from '../../utils/datetimeHelper';
|
||||
import { apiNotification } from '../common/apiNotification';
|
||||
|
@ -17,6 +19,7 @@ export interface EventProps {
|
|||
|
||||
const Event: FC<EventProps> = ({ entity, onDeleteSuccess }): JSX.Element => {
|
||||
const [t] = useTranslation('event');
|
||||
const { locale } = useLocaleProviderContext();
|
||||
|
||||
const [deleteEvent, { isSuccess, isLoading, isError, error }] = useDeleteEventMutation();
|
||||
|
||||
|
@ -44,7 +47,7 @@ const Event: FC<EventProps> = ({ entity, onDeleteSuccess }): JSX.Element => {
|
|||
size="small"
|
||||
actions={[
|
||||
<Text key={`${entity.id}_created`} italic type="secondary">
|
||||
{formatDateTimeWithTimeZone(entity.createdAt)}
|
||||
{formatDateTimeWithTimeZone(entity.createdAt, DateTimeStyle.LONG, DateTimeStyle.MEDIUM, locale)}
|
||||
</Text>,
|
||||
<Popconfirm
|
||||
key={`${entity.id}_del_confirm`}
|
||||
|
|
|
@ -2,6 +2,8 @@ import CreateSecret from './CreateSecret';
|
|||
import DeleteSecret from './DeleteSecret';
|
||||
import UpdateValueSecret from './UpdateValueSecret';
|
||||
import { useGetSecretsQuery } from '../../api/secretsApi';
|
||||
import DateTimeStyle from '../../constants/dateTimeStyle';
|
||||
import { useLocaleProviderContext } from '../../providers/LocaleContextProvider';
|
||||
import { SecretResponse } from '../../types';
|
||||
import { formatDateTimeWithTimeZone } from '../../utils/datetimeHelper';
|
||||
import { sortAlphaIgnoringCase } from '../../utils/sortHelper';
|
||||
|
@ -18,6 +20,7 @@ const { Text } = Typography;
|
|||
|
||||
const SecretsPage: FC = () => {
|
||||
const [t] = useTranslation('secrets');
|
||||
const { locale } = useLocaleProviderContext();
|
||||
|
||||
const { isLoading, isError, refetch, isFetching, isSuccess, data } = useGetSecretsQuery();
|
||||
|
||||
|
@ -52,7 +55,7 @@ const SecretsPage: FC = () => {
|
|||
ellipsis: true,
|
||||
responsive: ['sm', 'md', 'lg', 'xl', 'xxl'],
|
||||
sorter: (a, b) => sortAlphaIgnoringCase(a.createdAt, b.createdAt),
|
||||
render: (value) => formatDateTimeWithTimeZone(value)
|
||||
render: (value) => formatDateTimeWithTimeZone(value, DateTimeStyle.LONG, DateTimeStyle.MEDIUM, locale)
|
||||
},
|
||||
{
|
||||
title: t('col_updated_at'),
|
||||
|
@ -61,7 +64,7 @@ const SecretsPage: FC = () => {
|
|||
ellipsis: true,
|
||||
responsive: ['sm', 'md', 'lg', 'xl', 'xxl'],
|
||||
sorter: (a, b) => sortAlphaIgnoringCase(a.updatedAt, b.updatedAt),
|
||||
render: (value) => formatDateTimeWithTimeZone(value)
|
||||
render: (value) => formatDateTimeWithTimeZone(value, DateTimeStyle.LONG, DateTimeStyle.MEDIUM, locale)
|
||||
},
|
||||
{
|
||||
title: t('actions'),
|
||||
|
@ -73,7 +76,7 @@ const SecretsPage: FC = () => {
|
|||
}
|
||||
}
|
||||
];
|
||||
}, [t]);
|
||||
}, [locale, t]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import UpdateStateTag from './UpdateStateTag';
|
||||
import { useDeleteUpdateMutation, useModifyUpdateStateMutation } from '../../api/updatesApi';
|
||||
import AppPaths from '../../constants/appPaths';
|
||||
import DateTimeStyle from '../../constants/dateTimeStyle';
|
||||
import { useLocaleProviderContext } from '../../providers/LocaleContextProvider';
|
||||
import { UpdateResponse, UpdateState } from '../../types';
|
||||
import { formatDateTimeWithTimeZone } from '../../utils/datetimeHelper';
|
||||
import { getUpdateStateColor } from '../../utils/updateHelper';
|
||||
|
@ -28,6 +30,7 @@ export interface UpdateProps {
|
|||
|
||||
const Update: FC<UpdateProps> = ({ entity }): JSX.Element => {
|
||||
const [t] = useTranslation('update');
|
||||
const { locale } = useLocaleProviderContext();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const redirectToDetails = useCallback(() => {
|
||||
|
@ -178,7 +181,12 @@ const Update: FC<UpdateProps> = ({ entity }): JSX.Element => {
|
|||
<Tooltip
|
||||
placement={'bottom'}
|
||||
title={t('created_at', {
|
||||
created: formatDateTimeWithTimeZone(entity.createdAt)
|
||||
created: formatDateTimeWithTimeZone(
|
||||
entity.createdAt,
|
||||
DateTimeStyle.LONG,
|
||||
DateTimeStyle.MEDIUM,
|
||||
locale
|
||||
)
|
||||
})}>
|
||||
<FieldTimeOutlined />
|
||||
</Tooltip>
|
||||
|
@ -187,8 +195,18 @@ const Update: FC<UpdateProps> = ({ entity }): JSX.Element => {
|
|||
<Tooltip
|
||||
placement={'bottom'}
|
||||
title={t('created_at_diff', {
|
||||
created: formatDateTimeWithTimeZone(entity.createdAt),
|
||||
updated: formatDateTimeWithTimeZone(entity.updatedAt)
|
||||
created: formatDateTimeWithTimeZone(
|
||||
entity.createdAt,
|
||||
DateTimeStyle.LONG,
|
||||
DateTimeStyle.MEDIUM,
|
||||
locale
|
||||
),
|
||||
updated: formatDateTimeWithTimeZone(
|
||||
entity.updatedAt,
|
||||
DateTimeStyle.LONG,
|
||||
DateTimeStyle.MEDIUM,
|
||||
locale
|
||||
)
|
||||
})}>
|
||||
<FieldTimeOutlined />
|
||||
</Tooltip>
|
||||
|
@ -243,7 +261,14 @@ const Update: FC<UpdateProps> = ({ entity }): JSX.Element => {
|
|||
<Text type="secondary" ellipsis>
|
||||
{t('created')}
|
||||
</Text>
|
||||
<Text>{formatDateTimeWithTimeZone(entity.createdAt)}</Text>
|
||||
<Text>
|
||||
{formatDateTimeWithTimeZone(
|
||||
entity.createdAt,
|
||||
DateTimeStyle.LONG,
|
||||
DateTimeStyle.MEDIUM,
|
||||
locale
|
||||
)}
|
||||
</Text>
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
|
@ -253,7 +278,14 @@ const Update: FC<UpdateProps> = ({ entity }): JSX.Element => {
|
|||
<Text type="secondary" ellipsis>
|
||||
{t('updated')}
|
||||
</Text>
|
||||
<Text>{formatDateTimeWithTimeZone(entity.updatedAt)}</Text>
|
||||
<Text>
|
||||
{formatDateTimeWithTimeZone(
|
||||
entity.updatedAt,
|
||||
DateTimeStyle.LONG,
|
||||
DateTimeStyle.MEDIUM,
|
||||
locale
|
||||
)}
|
||||
</Text>
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
|
|
|
@ -4,6 +4,8 @@ import { useGetUpdateByIdQuery } from '../../api/updatesApi';
|
|||
import ApiErrorCodes from '../../constants/apiErrorCodes';
|
||||
import AppPathParamNames from '../../constants/appPathParamNames';
|
||||
import AppPaths from '../../constants/appPaths';
|
||||
import DateTimeStyle from '../../constants/dateTimeStyle';
|
||||
import { useLocaleProviderContext } from '../../providers/LocaleContextProvider';
|
||||
import { formatDateTimeWithTimeZone } from '../../utils/datetimeHelper';
|
||||
import { getPageFullPath } from '../../utils/urlHelper';
|
||||
import AppBreadcrumb from '../common/AppBreadcrumb';
|
||||
|
@ -17,6 +19,7 @@ const { Text } = Typography;
|
|||
|
||||
const UpdateSinglePage: FC = (): JSX.Element => {
|
||||
const [t] = useTranslation('updates_single');
|
||||
const { locale } = useLocaleProviderContext();
|
||||
|
||||
const { [AppPathParamNames.UPDATE_ID]: updateId } = useParams();
|
||||
|
||||
|
@ -76,10 +79,20 @@ const UpdateSinglePage: FC = (): JSX.Element => {
|
|||
<Descriptions.Item label={t('host')}>{data.data.host}</Descriptions.Item>
|
||||
<Descriptions.Item label={t('provider')}>{data.data.provider}</Descriptions.Item>
|
||||
<Descriptions.Item label={t('created')}>
|
||||
{formatDateTimeWithTimeZone(data.data.createdAt)}
|
||||
{formatDateTimeWithTimeZone(
|
||||
data.data.createdAt,
|
||||
DateTimeStyle.LONG,
|
||||
DateTimeStyle.MEDIUM,
|
||||
locale
|
||||
)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={t('updated')}>
|
||||
{formatDateTimeWithTimeZone(data.data.updatedAt)}
|
||||
{formatDateTimeWithTimeZone(
|
||||
data.data.updatedAt,
|
||||
DateTimeStyle.LONG,
|
||||
DateTimeStyle.MEDIUM,
|
||||
locale
|
||||
)}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
<Descriptions
|
||||
|
|
|
@ -3,7 +3,9 @@ import {
|
|||
useModifyIgnoreHostWebhookMutation,
|
||||
useModifyLabelWebhookMutation
|
||||
} from '../../api/webhooksApi';
|
||||
import DateTimeStyle from '../../constants/dateTimeStyle';
|
||||
import getConfiguration from '../../getConfiguration';
|
||||
import { useLocaleProviderContext } from '../../providers/LocaleContextProvider';
|
||||
import { WebhookResponse, WebhookType } from '../../types';
|
||||
import { formatDateTimeWithTimeZone } from '../../utils/datetimeHelper';
|
||||
import { apiNotification } from '../common/apiNotification';
|
||||
|
@ -21,6 +23,7 @@ export interface WebhookProps {
|
|||
|
||||
const Webhook: FC<WebhookProps> = ({ entity }): JSX.Element => {
|
||||
const [t] = useTranslation('webhook');
|
||||
const { locale } = useLocaleProviderContext();
|
||||
|
||||
const [deleteWebhook, { isLoading: isDeleteLoading, isError: isErrorDelete, error: deleteError }] =
|
||||
useDeleteWebhookMutation();
|
||||
|
@ -123,7 +126,12 @@ const Webhook: FC<WebhookProps> = ({ entity }): JSX.Element => {
|
|||
<Tooltip
|
||||
placement={'bottom'}
|
||||
title={t('created_at', {
|
||||
created: formatDateTimeWithTimeZone(entity.createdAt)
|
||||
created: formatDateTimeWithTimeZone(
|
||||
entity.createdAt,
|
||||
DateTimeStyle.LONG,
|
||||
DateTimeStyle.MEDIUM,
|
||||
locale
|
||||
)
|
||||
})}>
|
||||
<FieldTimeOutlined />
|
||||
</Tooltip>
|
||||
|
@ -132,8 +140,18 @@ const Webhook: FC<WebhookProps> = ({ entity }): JSX.Element => {
|
|||
<Tooltip
|
||||
placement={'bottom'}
|
||||
title={t('created_at_diff', {
|
||||
created: formatDateTimeWithTimeZone(entity.createdAt),
|
||||
updated: formatDateTimeWithTimeZone(entity.updatedAt)
|
||||
created: formatDateTimeWithTimeZone(
|
||||
entity.createdAt,
|
||||
DateTimeStyle.LONG,
|
||||
DateTimeStyle.MEDIUM,
|
||||
locale
|
||||
),
|
||||
updated: formatDateTimeWithTimeZone(
|
||||
entity.updatedAt,
|
||||
DateTimeStyle.LONG,
|
||||
DateTimeStyle.MEDIUM,
|
||||
locale
|
||||
)
|
||||
})}>
|
||||
<FieldTimeOutlined />
|
||||
</Tooltip>
|
||||
|
@ -193,7 +211,14 @@ const Webhook: FC<WebhookProps> = ({ entity }): JSX.Element => {
|
|||
<Text type="secondary" ellipsis>
|
||||
{t('created')}
|
||||
</Text>
|
||||
<Text>{formatDateTimeWithTimeZone(entity.createdAt)}</Text>
|
||||
<Text>
|
||||
{formatDateTimeWithTimeZone(
|
||||
entity.createdAt,
|
||||
DateTimeStyle.LONG,
|
||||
DateTimeStyle.MEDIUM,
|
||||
locale
|
||||
)}
|
||||
</Text>
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
|
@ -203,7 +228,14 @@ const Webhook: FC<WebhookProps> = ({ entity }): JSX.Element => {
|
|||
<Text type="secondary" ellipsis>
|
||||
{t('updated')}
|
||||
</Text>
|
||||
<Text>{formatDateTimeWithTimeZone(entity.updatedAt)}</Text>
|
||||
<Text>
|
||||
{formatDateTimeWithTimeZone(
|
||||
entity.updatedAt,
|
||||
DateTimeStyle.LONG,
|
||||
DateTimeStyle.MEDIUM,
|
||||
locale
|
||||
)}
|
||||
</Text>
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
|
|
111
server/web/src/providers/LocaleContextProvider.tsx
Normal file
111
server/web/src/providers/LocaleContextProvider.tsx
Normal file
|
@ -0,0 +1,111 @@
|
|||
import Languages from '../constants/languages';
|
||||
import Locales from '../constants/locales';
|
||||
import { Col, ConfigProvider, Layout, Row, Spin } from 'antd';
|
||||
import { Locale } from 'antd/lib/locale';
|
||||
import en_US from 'antd/lib/locale/en_US';
|
||||
// vite cannot bundle locales of moment, see https://github.com/moment/moment/issues/5926
|
||||
import moment from 'moment-timezone';
|
||||
import React, { createContext, FC, ReactNode, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export type LocaleProviderContextType = {
|
||||
language: string;
|
||||
locale: string;
|
||||
};
|
||||
|
||||
type LocaleProviderProviderProps = {
|
||||
children: ReactNode | ReactNode[];
|
||||
};
|
||||
|
||||
const LocaleContext = createContext<LocaleProviderContextType | undefined>(undefined);
|
||||
|
||||
const LocaleContextProvider: FC<LocaleProviderProviderProps> = ({ children }): React.JSX.Element => {
|
||||
const { i18n, t } = useTranslation();
|
||||
const [antLocale, setAntLocale] = useState<Locale>();
|
||||
const [language, setLanguage] = useState<string>(Languages.DEFAULT);
|
||||
const [locale, setLocale] = useState<string>(Locales.DEFAULT);
|
||||
|
||||
const isLocale = useCallback((i18nLanguage: string) => {
|
||||
return i18nLanguage.includes('-');
|
||||
}, []);
|
||||
|
||||
const getLocaleFromLanguage = useCallback((language: string) => {
|
||||
if (Languages.ENGLISH === language) {
|
||||
return Locales.ENGLISH_US;
|
||||
}
|
||||
|
||||
return Locales.DEFAULT;
|
||||
}, []);
|
||||
|
||||
const getLanguageFromLocale = useCallback((locale: string) => {
|
||||
const language = locale.split('-')[0];
|
||||
|
||||
if (Languages.ENGLISH === language) {
|
||||
return Languages.ENGLISH;
|
||||
}
|
||||
return Languages.DEFAULT;
|
||||
}, []);
|
||||
|
||||
const setLocales = useCallback(() => {
|
||||
let i18nLanguage = i18n.resolvedLanguage;
|
||||
let locale: string;
|
||||
let language: string;
|
||||
|
||||
if (!i18nLanguage) {
|
||||
i18nLanguage = Languages.DEFAULT;
|
||||
}
|
||||
|
||||
if (isLocale(i18nLanguage)) {
|
||||
language = getLanguageFromLocale(i18nLanguage);
|
||||
locale = getLocaleFromLanguage(language);
|
||||
} else {
|
||||
locale = getLocaleFromLanguage(i18nLanguage);
|
||||
language = getLanguageFromLocale(locale);
|
||||
}
|
||||
|
||||
if (Languages.ENGLISH === language) {
|
||||
setAntLocale(en_US);
|
||||
moment.updateLocale(language, null);
|
||||
} else {
|
||||
setAntLocale(en_US);
|
||||
}
|
||||
|
||||
setLocale(locale);
|
||||
setLanguage(language);
|
||||
}, [getLanguageFromLocale, getLocaleFromLanguage, i18n.resolvedLanguage, isLocale]);
|
||||
const renderProvider = useMemo(() => {
|
||||
return (
|
||||
<LocaleContext.Provider value={{ locale: locale, language: language }}>
|
||||
<ConfigProvider locale={antLocale}>{children}</ConfigProvider>
|
||||
</LocaleContext.Provider>
|
||||
);
|
||||
}, [antLocale, children, language, locale]);
|
||||
|
||||
useEffect(() => {
|
||||
setLocales();
|
||||
}, [setLocales, i18n]);
|
||||
|
||||
if (language && locale) {
|
||||
return renderProvider;
|
||||
}
|
||||
return (
|
||||
<Layout>
|
||||
<Layout.Content>
|
||||
<Row align="middle">
|
||||
<Col span={24}>
|
||||
<Spin tip={t('loading')} spinning />
|
||||
</Col>
|
||||
</Row>
|
||||
</Layout.Content>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
export const useLocaleProviderContext = (): LocaleProviderContextType => {
|
||||
const context = useContext(LocaleContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useLocaleProviderContext must be used within LocaleProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export default LocaleContextProvider;
|
|
@ -1,6 +1,17 @@
|
|||
import moment from 'moment-timezone';
|
||||
import DateTimeStyle from '../constants/dateTimeStyle';
|
||||
|
||||
export const formatDateTimeWithTimeZone = (date: number | string, timezone = moment.tz.guess()) => {
|
||||
const d = new Intl.DateTimeFormat('en-US', { dateStyle: 'medium', timeStyle: 'long', timeZone: timezone });
|
||||
return d.format(new Date(date));
|
||||
export const formatDateTimeWithTimeZone = (
|
||||
date: number | string,
|
||||
timeStyle: DateTimeStyle,
|
||||
dateStyle: DateTimeStyle,
|
||||
locale: string,
|
||||
timezone = moment.tz.guess()
|
||||
) => {
|
||||
const dtf = new Intl.DateTimeFormat(locale, {
|
||||
timeStyle: timeStyle,
|
||||
dateStyle: dateStyle,
|
||||
timeZone: timezone
|
||||
});
|
||||
return dtf.format(new Date(date));
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue