mirror of
https://github.com/logto-io/logto.git
synced 2025-03-10 22:22:45 -05:00
feat(console): manage language (#1981)
This commit is contained in:
parent
f25ae4de14
commit
48832e5054
30 changed files with 657 additions and 20 deletions
|
@ -41,4 +41,8 @@
|
|||
&.large {
|
||||
max-width: dim.$modal-layout-width-large;
|
||||
}
|
||||
|
||||
&.xlarge {
|
||||
max-width: dim.$modal-layout-width-xlarge;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ type Props = {
|
|||
footer?: ReactNode;
|
||||
onClose?: () => void;
|
||||
className?: string;
|
||||
size?: 'medium' | 'large';
|
||||
size?: 'medium' | 'large' | 'xlarge';
|
||||
};
|
||||
|
||||
const ModalLayout = ({
|
||||
|
|
|
@ -1,18 +1,23 @@
|
|||
import { languageOptions } from '@logto/phrases-ui';
|
||||
import classNames from 'classnames';
|
||||
import { useState } from 'react';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import FormField from '@/components/FormField';
|
||||
import Select from '@/components/Select';
|
||||
import Switch from '@/components/Switch';
|
||||
import * as textButtonStyles from '@/components/TextButton/index.module.scss';
|
||||
|
||||
import { SignInExperienceForm } from '../types';
|
||||
import ManageLanguageModal from './ManageLanguageModal';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
const LanguagesForm = () => {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const { watch, control, register } = useFormContext<SignInExperienceForm>();
|
||||
const isAutoDetect = watch('languageInfo.autoDetect');
|
||||
const [isManageLanguageFormOpen, setIsManageLanguageFormOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -23,6 +28,14 @@ const LanguagesForm = () => {
|
|||
label={t('sign_in_exp.others.languages.description')}
|
||||
/>
|
||||
</FormField>
|
||||
<div
|
||||
className={classNames(textButtonStyles.button, styles.manageLanguage)}
|
||||
onClick={() => {
|
||||
setIsManageLanguageFormOpen(true);
|
||||
}}
|
||||
>
|
||||
{t('sign_in_exp.others.languages.manage_language')}
|
||||
</div>
|
||||
<FormField title="sign_in_exp.others.languages.default_language">
|
||||
<Controller
|
||||
name="languageInfo.fallbackLanguage"
|
||||
|
@ -37,6 +50,12 @@ const LanguagesForm = () => {
|
|||
: t('sign_in_exp.others.languages.default_language_description_fixed')}
|
||||
</div>
|
||||
</FormField>
|
||||
<ManageLanguageModal
|
||||
isOpen={isManageLanguageFormOpen}
|
||||
onClose={() => {
|
||||
setIsManageLanguageFormOpen(false);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.sectionTitle {
|
||||
@include _.subhead-cap;
|
||||
background-color: var(--color-layer-light);
|
||||
}
|
||||
|
||||
.sectionDataKey {
|
||||
padding: _.unit(4) _.unit(5);
|
||||
font: var(--font-body-medium);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.sectionBuiltInText {
|
||||
padding: _.unit(2) 0;
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
import type { Translation as UiTranslation } from '@logto/phrases-ui';
|
||||
import { FieldPath, useFormContext } from 'react-hook-form';
|
||||
|
||||
import TextInput from '@/components/TextInput';
|
||||
|
||||
import * as style from './EditSection.module.scss';
|
||||
|
||||
type EditSectionProps = {
|
||||
dataKey: string;
|
||||
data: Record<string, unknown>;
|
||||
};
|
||||
|
||||
const EditSection = ({ dataKey, data }: EditSectionProps) => {
|
||||
const { register } = useFormContext<UiTranslation>();
|
||||
|
||||
return (
|
||||
<>
|
||||
<tr>
|
||||
<td colSpan={3} className={style.sectionTitle}>
|
||||
{dataKey}
|
||||
</td>
|
||||
</tr>
|
||||
{Object.entries(data).map(([field, value]) => {
|
||||
if (typeof value !== 'string') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const fieldKey = `${dataKey}.${field}`;
|
||||
|
||||
return (
|
||||
<tr key={fieldKey}>
|
||||
<td className={style.sectionDataKey}>{field}</td>
|
||||
<td>
|
||||
<TextInput readOnly value={value} className={style.sectionBuiltInText} />
|
||||
</td>
|
||||
<td>
|
||||
{
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
<TextInput {...register(fieldKey as FieldPath<UiTranslation>)} />
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditSection;
|
|
@ -0,0 +1,92 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.languageEditor {
|
||||
flex-grow: 1;
|
||||
|
||||
.title {
|
||||
padding: _.unit(6) _.unit(5);
|
||||
font: var(--font-title-large);
|
||||
color: var(--color-text);
|
||||
|
||||
> span {
|
||||
margin-left: _.unit(2);
|
||||
font: var(--font-body-medium);
|
||||
color: var(--color-caption);
|
||||
}
|
||||
|
||||
.builtInFlag {
|
||||
display: inline-block;
|
||||
font: var(--font-label-medium);
|
||||
color: var(--color-text);
|
||||
background-color: var(--color-surface-variant);
|
||||
padding: _.unit(0.5) _.unit(2);
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
border-top: 1px solid var(--color-border);
|
||||
height: 481px;
|
||||
overflow-y: auto;
|
||||
|
||||
> table {
|
||||
border: none;
|
||||
|
||||
> thead > tr {
|
||||
> th {
|
||||
font: var(--font-label-large);
|
||||
color: var(--color-text);
|
||||
background-color: var(--color-layer-1);
|
||||
}
|
||||
|
||||
> th:first-child {
|
||||
width: 300px;
|
||||
padding: _.unit(1) _.unit(5);
|
||||
}
|
||||
}
|
||||
|
||||
> tbody > tr {
|
||||
> td {
|
||||
border: none;
|
||||
}
|
||||
|
||||
> td:first-child {
|
||||
padding: _.unit(1) _.unit(5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.customValuesColumn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.clearButton {
|
||||
display: flex;
|
||||
margin-left: _.unit(2);
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
@include _.subhead-cap;
|
||||
background-color: var(--color-layer-light);
|
||||
}
|
||||
|
||||
.sectionDataKey {
|
||||
padding: _.unit(4) _.unit(5);
|
||||
font: var(--font-body-medium);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.sectionBuiltInText {
|
||||
padding: _.unit(2) 0;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
border-top: 1px solid var(--color-border);
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
padding: _.unit(5);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,140 @@
|
|||
import type { LanguageKey } from '@logto/core-kit';
|
||||
import resource, {
|
||||
languageCodeAndDisplayNameMappings,
|
||||
Translation as UiTranslation,
|
||||
} from '@logto/phrases-ui';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import Button from '@/components/Button';
|
||||
import useApi, { RequestError } from '@/hooks/use-api';
|
||||
import Delete from '@/icons/Delete';
|
||||
|
||||
import { createEmptyUiTranslation, flattenObject } from '../../utilities';
|
||||
import EditSection from './EditSection';
|
||||
import * as style from './LanguageEditor.module.scss';
|
||||
import { CustomPhraseResponse } from './types';
|
||||
|
||||
type LanguageEditorProps = {
|
||||
selectedLanguageKey: LanguageKey;
|
||||
};
|
||||
|
||||
const emptyUiTranslation = createEmptyUiTranslation();
|
||||
|
||||
const LanguageEditor = ({ selectedLanguageKey }: LanguageEditorProps) => {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
|
||||
const isBuiltInLanguage = Object.keys(resource).includes(selectedLanguageKey);
|
||||
const translationEntries = useMemo(
|
||||
() => Object.entries(resource[selectedLanguageKey].translation),
|
||||
[selectedLanguageKey]
|
||||
);
|
||||
|
||||
const api = useApi();
|
||||
|
||||
const { data: customPhrase, mutate } = useSWR<CustomPhraseResponse, RequestError>(
|
||||
`/api/custom-phrases/${selectedLanguageKey}`,
|
||||
{
|
||||
shouldRetryOnError: (error: unknown) => {
|
||||
if (error instanceof RequestError) {
|
||||
return error.status !== 404;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const formMethods = useForm<UiTranslation>();
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { isSubmitting },
|
||||
} = formMethods;
|
||||
|
||||
const onSubmit = handleSubmit(async (formData: UiTranslation) => {
|
||||
const updatedCustomPhrase = await api
|
||||
.put(`/api/custom-phrases/${selectedLanguageKey}`, {
|
||||
json: {
|
||||
...formData,
|
||||
},
|
||||
})
|
||||
.json<CustomPhraseResponse>();
|
||||
|
||||
void mutate(updatedCustomPhrase);
|
||||
toast.success(t('general.saved'));
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
reset(customPhrase?.translation ?? emptyUiTranslation);
|
||||
}, [customPhrase, reset]);
|
||||
|
||||
return (
|
||||
<div className={style.languageEditor}>
|
||||
<div className={style.title}>
|
||||
{languageCodeAndDisplayNameMappings[selectedLanguageKey]}
|
||||
<span>{selectedLanguageKey}</span>
|
||||
{isBuiltInLanguage && (
|
||||
<span className={style.builtInFlag}>
|
||||
{t('sign_in_exp.others.manage_language.logto_provided')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<form
|
||||
onSubmit={async (event) => {
|
||||
// Note: Avoid propagating the 'submit' event to the outer sign-in-experience form.
|
||||
event.stopPropagation();
|
||||
|
||||
return onSubmit(event);
|
||||
}}
|
||||
>
|
||||
<div className={style.content}>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t('sign_in_exp.others.manage_language.key')}</th>
|
||||
<th>{t('sign_in_exp.others.manage_language.logto_source_language')}</th>
|
||||
<th>
|
||||
<span className={style.customValuesColumn}>
|
||||
{t('sign_in_exp.others.manage_language.custom_values')}
|
||||
<Button
|
||||
type="plain"
|
||||
title="sign_in_exp.others.manage_language.clear_all"
|
||||
className={style.clearButton}
|
||||
icon={<Delete />}
|
||||
onClick={() => {
|
||||
reset(emptyUiTranslation);
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<FormProvider {...formMethods}>
|
||||
{translationEntries.map(([key, value]) => (
|
||||
<EditSection key={key} dataKey={key} data={flattenObject(value)} />
|
||||
))}
|
||||
</FormProvider>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className={style.footer}>
|
||||
<Button
|
||||
isLoading={isSubmitting}
|
||||
htmlType="submit"
|
||||
type="primary"
|
||||
size="large"
|
||||
title="general.save"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LanguageEditor;
|
|
@ -0,0 +1,31 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.languageItem {
|
||||
padding: _.unit(1.5) _.unit(3);
|
||||
margin-bottom: _.unit(1);
|
||||
cursor: pointer;
|
||||
border-radius: 8px;
|
||||
|
||||
.languageName {
|
||||
font: var(--font-title-medium);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.languageKey {
|
||||
font: var(--font-label-large);
|
||||
color: var(--color-caption);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-hover-variant);
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background-color: var(--color-focused-variant);
|
||||
|
||||
.languageName,
|
||||
.languageKey {
|
||||
color: var(--color-text-link);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
import { LanguageKey } from '@logto/core-kit';
|
||||
import { languageCodeAndDisplayNameMappings } from '@logto/phrases-ui';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import * as style from './LanguageItem.module.scss';
|
||||
|
||||
type Props = {
|
||||
languageKey: LanguageKey;
|
||||
isSelected: boolean;
|
||||
onClick: () => void;
|
||||
};
|
||||
|
||||
const LanguageItem = ({ languageKey, isSelected, onClick }: Props) => {
|
||||
return (
|
||||
<div className={classNames(style.languageItem, isSelected && style.selected)} onClick={onClick}>
|
||||
<div className={style.languageName}>{languageCodeAndDisplayNameMappings[languageKey]}</div>
|
||||
<div className={style.languageKey}>{languageKey}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LanguageItem;
|
|
@ -0,0 +1,21 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.languageNav {
|
||||
width: 185px;
|
||||
padding: _.unit(3) _.unit(2);
|
||||
flex-shrink: 0;
|
||||
background-color: var(--color-layer-light);
|
||||
border-right: 1px solid var(--color-border);
|
||||
|
||||
.addLanguageButton {
|
||||
width: 100%;
|
||||
border-color: var(--color-outline);
|
||||
color: var(--color-text);
|
||||
background-color: unset;
|
||||
margin-bottom: _.unit(3);
|
||||
|
||||
.iconPlus {
|
||||
color: var(--color-outline);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
import { LanguageKey } from '@logto/core-kit';
|
||||
|
||||
import Button from '@/components/Button';
|
||||
import Plus from '@/icons/Plus';
|
||||
|
||||
import LanguageItem from './LanguageItem';
|
||||
import * as style from './LanguageNav.module.scss';
|
||||
|
||||
type Props = {
|
||||
languageKeys: LanguageKey[];
|
||||
selectedLanguage: LanguageKey;
|
||||
onSelect: (languageKey: LanguageKey) => void;
|
||||
};
|
||||
|
||||
const LanguageNav = ({ languageKeys, selectedLanguage, onSelect }: Props) => {
|
||||
// TODO: LOG-4146 Add Custom Language
|
||||
return (
|
||||
<div className={style.languageNav}>
|
||||
<Button
|
||||
className={style.addLanguageButton}
|
||||
icon={<Plus className={style.iconPlus} />}
|
||||
title="sign_in_exp.others.manage_language.add_language"
|
||||
type="outline"
|
||||
size="medium"
|
||||
/>
|
||||
<div>
|
||||
{languageKeys.map((key) => (
|
||||
<LanguageItem
|
||||
key={key}
|
||||
languageKey={key}
|
||||
isSelected={selectedLanguage === key}
|
||||
onClick={() => {
|
||||
onSelect(key);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LanguageNav;
|
|
@ -0,0 +1,7 @@
|
|||
.container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
import { getDefaultLanguage, LanguageKey } from '@logto/core-kit';
|
||||
import { languageOptions as uiLanguageOptions } from '@logto/phrases-ui';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import Modal from 'react-modal';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import ModalLayout from '@/components/ModalLayout';
|
||||
import { RequestError } from '@/hooks/use-api';
|
||||
import * as modalStyles from '@/scss/modal.module.scss';
|
||||
|
||||
import LanguageEditor from './LanguageEditor';
|
||||
import LanguageNav from './LanguageNav';
|
||||
import * as style from './index.module.scss';
|
||||
import { CustomPhraseResponse } from './types';
|
||||
|
||||
type ManageLanguageModalProps = {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const ManageLanguageModal = ({ isOpen, onClose }: ManageLanguageModalProps) => {
|
||||
const { data: customPhraseResponses } = useSWR<CustomPhraseResponse[], RequestError>(
|
||||
'/api/custom-phrases'
|
||||
);
|
||||
|
||||
const allLanguageKeys = useMemo(() => {
|
||||
const uiBuiltInLanguageKeys = uiLanguageOptions.map((option) => option.value);
|
||||
const customUiLanguageKeys = customPhraseResponses?.map(({ languageKey }) => languageKey);
|
||||
|
||||
const allKeys = customUiLanguageKeys?.length
|
||||
? [...new Set([...uiBuiltInLanguageKeys, ...customUiLanguageKeys])]
|
||||
: uiBuiltInLanguageKeys;
|
||||
|
||||
return allKeys.slice().sort();
|
||||
}, [customPhraseResponses]);
|
||||
|
||||
const defaultLanguageKey = getDefaultLanguage(allLanguageKeys[0] ?? '');
|
||||
|
||||
const [selectedLanguageKey, setSelectedLanguageKey] = useState<LanguageKey>(defaultLanguageKey);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
setSelectedLanguageKey(defaultLanguageKey);
|
||||
}
|
||||
}, [allLanguageKeys, setSelectedLanguageKey, isOpen, defaultLanguageKey]);
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} className={modalStyles.content} overlayClassName={modalStyles.overlay}>
|
||||
<ModalLayout
|
||||
title="sign_in_exp.others.manage_language.title"
|
||||
subtitle="sign_in_exp.others.manage_language.subtitle"
|
||||
size="xlarge"
|
||||
onClose={onClose}
|
||||
>
|
||||
<div className={style.container}>
|
||||
<LanguageNav
|
||||
languageKeys={allLanguageKeys}
|
||||
selectedLanguage={selectedLanguageKey}
|
||||
onSelect={setSelectedLanguageKey}
|
||||
/>
|
||||
<LanguageEditor selectedLanguageKey={selectedLanguageKey} />
|
||||
</div>
|
||||
</ModalLayout>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ManageLanguageModal;
|
|
@ -0,0 +1,7 @@
|
|||
import type { LanguageKey } from '@logto/core-kit';
|
||||
import type { Translation as UiTranslation } from '@logto/phrases-ui';
|
||||
|
||||
export type CustomPhraseResponse = {
|
||||
languageKey: LanguageKey;
|
||||
translation: UiTranslation;
|
||||
};
|
|
@ -29,6 +29,10 @@
|
|||
color: var(--color-caption);
|
||||
}
|
||||
|
||||
.manageLanguage {
|
||||
margin-top: _.unit(2);
|
||||
}
|
||||
|
||||
.defaultLanguageDescription {
|
||||
padding-top: _.unit(2);
|
||||
font: var(--font-body-medium);
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import type { Translation as UiTranslation } from '@logto/phrases-ui';
|
||||
import en from '@logto/phrases-ui/lib/locales/en';
|
||||
import {
|
||||
SignInExperience,
|
||||
SignInMethodKey,
|
||||
|
@ -98,3 +100,48 @@ export const compareSignInMethods = (
|
|||
|
||||
return Object.values(SignInMethodKey).every((key) => beforeMethods[key] === afterMethods[key]);
|
||||
};
|
||||
|
||||
// TODO: LOG-4235 move this method into @silverhand/essentials
|
||||
const isObject = (data: unknown): data is Record<string, unknown> =>
|
||||
typeof data === 'object' && !Array.isArray(data);
|
||||
|
||||
export const flattenObject = (
|
||||
object: Record<string, unknown>,
|
||||
keyPrefix = ''
|
||||
): Record<string, unknown> => {
|
||||
return Object.keys(object).reduce((result, key) => {
|
||||
const prefix = keyPrefix ? `${keyPrefix}.` : keyPrefix;
|
||||
const dataKey = `${prefix}${key}`;
|
||||
const data = object[key];
|
||||
|
||||
return {
|
||||
...result,
|
||||
...(isObject(data) ? flattenObject(data, dataKey) : { [dataKey]: data }),
|
||||
};
|
||||
}, {});
|
||||
};
|
||||
|
||||
const emptyTranslation = (translation: Record<string, unknown>): Record<string, unknown> => {
|
||||
return Object.entries(translation).reduce<Record<string, unknown>>((result, [key, value]) => {
|
||||
if (isObject(value)) {
|
||||
return {
|
||||
...result,
|
||||
[key]: emptyTranslation(value),
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
return {
|
||||
...result,
|
||||
[key]: '',
|
||||
};
|
||||
}
|
||||
|
||||
return { ...result, [key]: value };
|
||||
}, {});
|
||||
};
|
||||
|
||||
export const createEmptyUiTranslation = () => {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
return emptyTranslation(en.translation) as UiTranslation;
|
||||
};
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
$modal-layout-width-xlarge: 1224px;
|
||||
$modal-layout-width-large: 784px;
|
||||
$modal-layout-width-medium: 600px;
|
||||
$modal-layout-width-small: 352px;
|
||||
|
|
|
@ -6,11 +6,11 @@ import koKR from './locales/ko-kr';
|
|||
import ptPT from './locales/pt-pt';
|
||||
import trTR from './locales/tr-tr';
|
||||
import zhCN from './locales/zh-cn';
|
||||
import { Resource } from './types';
|
||||
import { Resource, Translation } from './types';
|
||||
|
||||
export { languageOptions } from './types';
|
||||
export * from './types';
|
||||
|
||||
export type I18nKey = NormalizeKeyPaths<typeof en.translation>;
|
||||
export type I18nKey = NormalizeKeyPaths<Translation>;
|
||||
|
||||
const resource: Resource = {
|
||||
en,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import en from './en';
|
||||
import { LocalePhrase } from '../types';
|
||||
|
||||
const translation = {
|
||||
input: {
|
||||
|
@ -84,7 +84,7 @@ const translation = {
|
|||
},
|
||||
};
|
||||
|
||||
const fr: typeof en = Object.freeze({
|
||||
const fr: LocalePhrase = Object.freeze({
|
||||
translation,
|
||||
});
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import en from './en';
|
||||
import { LocalePhrase } from '../types';
|
||||
|
||||
const translation = {
|
||||
input: {
|
||||
|
@ -77,7 +77,7 @@ const translation = {
|
|||
},
|
||||
};
|
||||
|
||||
const koKR: typeof en = Object.freeze({
|
||||
const koKR: LocalePhrase = Object.freeze({
|
||||
translation,
|
||||
});
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import en from './en';
|
||||
import { LocalePhrase } from '../types';
|
||||
|
||||
const translation = {
|
||||
input: {
|
||||
|
@ -78,7 +78,7 @@ const translation = {
|
|||
},
|
||||
};
|
||||
|
||||
const ptPT: typeof en = Object.freeze({
|
||||
const ptPT: LocalePhrase = Object.freeze({
|
||||
translation,
|
||||
});
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import en from './en';
|
||||
import { LocalePhrase } from '../types';
|
||||
|
||||
const translation = {
|
||||
input: {
|
||||
|
@ -78,7 +78,7 @@ const translation = {
|
|||
},
|
||||
};
|
||||
|
||||
const trTR: typeof en = Object.freeze({
|
||||
const trTR: LocalePhrase = Object.freeze({
|
||||
translation,
|
||||
});
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import en from './en';
|
||||
import { LocalePhrase } from '../types';
|
||||
|
||||
const translation = {
|
||||
input: {
|
||||
|
@ -77,7 +77,7 @@ const translation = {
|
|||
},
|
||||
};
|
||||
|
||||
const zhCN: typeof en = Object.freeze({
|
||||
const zhCN: LocalePhrase = Object.freeze({
|
||||
translation,
|
||||
});
|
||||
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
import { LanguageKey, languageKeyGuard } from '@logto/core-kit';
|
||||
|
||||
import en from './locales/en';
|
||||
|
||||
export type Translation = typeof en.translation;
|
||||
export type LocalePhrase = { translation: Translation };
|
||||
|
||||
/* Copied from i18next/index.d.ts */
|
||||
export type Resource = Record<LanguageKey, ResourceLanguage>;
|
||||
export type Resource = Record<LanguageKey, LocalePhrase>;
|
||||
|
||||
export type ResourceLanguage = Record<string, ResourceKey>;
|
||||
|
||||
export type ResourceKey = string | Record<string, unknown>;
|
||||
|
||||
const languageCodeAndDisplayNameMappings: Record<LanguageKey, string> = {
|
||||
export const languageCodeAndDisplayNameMappings: Record<LanguageKey, string> = {
|
||||
en: 'English',
|
||||
fr: 'Français',
|
||||
'pt-PT': 'Português',
|
||||
|
|
|
@ -81,6 +81,17 @@ const sign_in_exp = {
|
|||
default_language_description_fixed:
|
||||
'When auto detect is off, the default language is the only language your software will show. Turn on auto detect for language customization.',
|
||||
},
|
||||
manage_language: {
|
||||
title: 'Manage language',
|
||||
subtitle:
|
||||
'Localize the product experience by adding languages and translations. Your contribution can be set as the default language.',
|
||||
add_language: 'Add Language',
|
||||
logto_provided: 'Logto provided',
|
||||
key: 'Key',
|
||||
logto_source_language: 'Logto source language',
|
||||
custom_values: 'Custom values',
|
||||
clear_all: 'Clear all',
|
||||
},
|
||||
authentication: {
|
||||
title: 'AUTHENTICATION',
|
||||
enable_create_account: 'Enable create account',
|
||||
|
|
|
@ -83,6 +83,17 @@ const sign_in_exp = {
|
|||
default_language_description_fixed:
|
||||
'When auto detect is off, the default language is the only language your software will show. Turn on auto detect for language customization.', // UNTRANSLATED
|
||||
},
|
||||
manage_language: {
|
||||
title: 'Manage language', // UNTRANSLATED
|
||||
subtitle:
|
||||
'Localize the product experience by adding languages and translations. Your contribution can be set as the default language.', // UNTRANSLATED
|
||||
add_language: 'Add Language', // UNTRANSLATED
|
||||
logto_provided: 'Logto provided', // UNTRANSLATED
|
||||
key: 'Key', // UNTRANSLATED
|
||||
logto_source_language: 'Logto source language', // UNTRANSLATED
|
||||
custom_values: 'Custom values', // UNTRANSLATED
|
||||
clear_all: 'Clear all', // UNTRANSLATED
|
||||
},
|
||||
authentication: {
|
||||
title: 'AUTHENTICATION',
|
||||
enable_create_account: 'Enable create account',
|
||||
|
|
|
@ -78,6 +78,17 @@ const sign_in_exp = {
|
|||
default_language_description_fixed:
|
||||
'When auto detect is off, the default language is the only language your software will show. Turn on auto detect for language customization.', // UNTRANSLATED
|
||||
},
|
||||
manage_language: {
|
||||
title: 'Manage language', // UNTRANSLATED
|
||||
subtitle:
|
||||
'Localize the product experience by adding languages and translations. Your contribution can be set as the default language.', // UNTRANSLATED
|
||||
add_language: 'Add Language', // UNTRANSLATED
|
||||
logto_provided: 'Logto provided', // UNTRANSLATED
|
||||
key: 'Key', // UNTRANSLATED
|
||||
logto_source_language: 'Logto source language', // UNTRANSLATED
|
||||
custom_values: 'Custom values', // UNTRANSLATED
|
||||
clear_all: 'Clear all', // UNTRANSLATED
|
||||
},
|
||||
authentication: {
|
||||
title: 'AUTHENTICATION',
|
||||
enable_create_account: 'Enable create account',
|
||||
|
|
|
@ -81,6 +81,17 @@ const sign_in_exp = {
|
|||
default_language_description_fixed:
|
||||
'When auto detect is off, the default language is the only language your software will show. Turn on auto detect for language customization.', // UNTRANSLATED
|
||||
},
|
||||
manage_language: {
|
||||
title: 'Manage language', // UNTRANSLATED
|
||||
subtitle:
|
||||
'Localize the product experience by adding languages and translations. Your contribution can be set as the default language.', // UNTRANSLATED
|
||||
add_language: 'Add Language', // UNTRANSLATED
|
||||
logto_provided: 'Logto provided', // UNTRANSLATED
|
||||
key: 'Key', // UNTRANSLATED
|
||||
logto_source_language: 'Logto source language', // UNTRANSLATED
|
||||
custom_values: 'Custom values', // UNTRANSLATED
|
||||
clear_all: 'Clear all', // UNTRANSLATED
|
||||
},
|
||||
authentication: {
|
||||
title: 'AUTENTICAÇÃO',
|
||||
enable_create_account: 'Permitir criar conta?',
|
||||
|
|
|
@ -82,6 +82,17 @@ const sign_in_exp = {
|
|||
default_language_description_fixed:
|
||||
'When auto detect is off, the default language is the only language your software will show. Turn on auto detect for language customization.', // UNTRANSLATED
|
||||
},
|
||||
manage_language: {
|
||||
title: 'Manage language', // UNTRANSLATED
|
||||
subtitle:
|
||||
'Localize the product experience by adding languages and translations. Your contribution can be set as the default language.', // UNTRANSLATED
|
||||
add_language: 'Add Language', // UNTRANSLATED
|
||||
logto_provided: 'Logto provided', // UNTRANSLATED
|
||||
key: 'Key', // UNTRANSLATED
|
||||
logto_source_language: 'Logto source language', // UNTRANSLATED
|
||||
custom_values: 'Custom values', // UNTRANSLATED
|
||||
clear_all: 'Clear all', // UNTRANSLATED
|
||||
},
|
||||
authentication: {
|
||||
title: 'AUTHENTICATION',
|
||||
enable_create_account: 'Enable create account',
|
||||
|
|
|
@ -78,6 +78,17 @@ const sign_in_exp = {
|
|||
default_language_description_fixed:
|
||||
'When auto detect is off, the default language is the only language your software will show. Turn on auto detect for language customization.', // UNTRANSLATED
|
||||
},
|
||||
manage_language: {
|
||||
title: 'Manage language', // UNTRANSLATED
|
||||
subtitle:
|
||||
'Localize the product experience by adding languages and translations. Your contribution can be set as the default language.', // UNTRANSLATED
|
||||
add_language: 'Add Language', // UNTRANSLATED
|
||||
logto_provided: 'Logto provided', // UNTRANSLATED
|
||||
key: 'Key', // UNTRANSLATED
|
||||
logto_source_language: 'Logto source language', // UNTRANSLATED
|
||||
custom_values: 'Custom values', // UNTRANSLATED
|
||||
clear_all: 'Clear all', // UNTRANSLATED
|
||||
},
|
||||
authentication: {
|
||||
title: '身份验证',
|
||||
enable_create_account: '启用创建帐号',
|
||||
|
|
Loading…
Add table
Reference in a new issue