feat: RTL Support (#3003)
Co-authored-by: Alessio Gravili <alessio@gravili.de> Co-authored-by: ibr-hin95 <ibr.hin95@gmail.com> Co-authored-by: Seied Ali Mirkarimi <dasmergo@gmail.com> Co-authored-by: muathkhatib <mkhatib.dev@gmail.com> Co-authored-by: ibr-hin95 <40246707+ibr-hin95@users.noreply.github.com> fix: recursiveNestedPaths not merging existing fields when hoisting row/collapsible fields (#2769) fix: exclude monaco code editor from ltr due to microsoft/monaco-editor#2371 BREAKING CHANGE: - The admin hook for `useLocale` now returns a Locale object of the currently active locale. Previously this would only return the code as a string. Any custom components built which had `locale = useLocale()` should be replaced with `{ code: locale } = useLocale()` to maintain the same functionality. - The property `localization.locales` of `SanitizedConfig` type has changed. This was an array of strings and is now an array of Locale objects having: `label: string`, `code: string` and `rtl: boolean`. If you are using localization.locales from the config you will need to adjust your project or plugin accordingly.
This commit is contained in:
@@ -351,7 +351,7 @@ const CustomComponent: React.FC = () => {
|
||||
|
||||
### Getting the current locale
|
||||
|
||||
In any custom component you can get the selected locale with the `useLocale` hook. Here is a simple example:
|
||||
In any custom component you can get the selected locale with `useLocale` hook. `useLocale` returns the full locale object, consisting of a `label`, `rtl`(right-to-left) property, and then `code`. Here is a simple example:
|
||||
|
||||
```tsx
|
||||
import { useLocale } from "payload/components/utilities";
|
||||
@@ -366,6 +366,6 @@ const Greeting: React.FC = () => {
|
||||
es: "Hola",
|
||||
};
|
||||
|
||||
return <span> {trans[locale]} </span>;
|
||||
return <span> {trans[locale.code]} </span>
|
||||
};
|
||||
```
|
||||
|
||||
@@ -662,7 +662,7 @@ const LinkFromCategoryToPosts: React.FC = () => {
|
||||
|
||||
### useLocale
|
||||
|
||||
In any custom component you can get the selected locale with the `useLocale` hook. Here is a simple example:
|
||||
In any custom component you can get the selected locale object with the `useLocale` hook. `useLocale`gives you the full locale object, consisting of a `label`, `rtl`(right-to-left) property, and then `code`. Here is a simple example:
|
||||
|
||||
```tsx
|
||||
import { useLocale } from 'payload/components/utilities';
|
||||
@@ -678,7 +678,7 @@ const Greeting: React.FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<span> { trans[locale] } </span>
|
||||
<span> { trans[locale.code] } </span>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
@@ -33,15 +33,43 @@ export default buildConfig({
|
||||
});
|
||||
```
|
||||
|
||||
**Example Payload config set up for localization with full locales objects:**
|
||||
|
||||
```ts
|
||||
import { buildConfig } from 'payload/config'
|
||||
|
||||
export default buildConfig({
|
||||
collections: [
|
||||
// collections go here
|
||||
],
|
||||
localization: {
|
||||
locales: [
|
||||
{
|
||||
label: "English",
|
||||
code: "en",
|
||||
},
|
||||
{
|
||||
label: "Arabic",
|
||||
code: "ar",
|
||||
// opt-in to setting default text-alignment on Input fields to rtl (right-to-left) when current locale is rtl
|
||||
rtl: true,
|
||||
},
|
||||
],
|
||||
defaultLocale: "en",
|
||||
fallback: true,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
**Here is a brief explanation of each of the options available within the `localization` property:**
|
||||
|
||||
**`locales`**
|
||||
|
||||
Array-based list of all locales that you would like to support. These strings do not need to be in any specific format. It's up to you to define how to represent your locales. Common patterns are to use two-letter ISO 639 language codes or four-letter language and country codes (ISO 3166‑1) such as `en-US`, `en-UK`, `es-MX`, etc.
|
||||
Array-based list of all locales that you would like to support. These can be strings of locale codes or objects with a `label`, a locale `code`, and the `rtl` (right-to-left) property. The locale codes do not need to be in any specific format. It's up to you to define how to represent your locales. Common patterns are to use two-letter ISO 639 language codes or four-letter language and country codes (ISO 3166‑1) such as `en-US`, `en-UK`, `es-MX`, etc.
|
||||
|
||||
**`defaultLocale`**
|
||||
|
||||
Required string that matches one of the locales from the array provided. By default, if no locale is specified, documents will be returned in this locale.
|
||||
Required string that matches one of the locale codes from the array provided. By default, if no locale is specified, documents will be returned in this locale.
|
||||
|
||||
**`fallback`**
|
||||
|
||||
|
||||
@@ -116,6 +116,10 @@ _RichText field using the upload element_
|
||||

|
||||
_RichText upload element modal displaying fields from the config_
|
||||
|
||||
**`rtl`**
|
||||
|
||||
Override the default text direction of the Admin panel for this field. Set to `true` to force right-to-left text direction.
|
||||
|
||||
### Relationship element
|
||||
|
||||
The built-in `relationship` element is a powerful way to reference other Documents directly within your Rich Text editor.
|
||||
|
||||
@@ -52,6 +52,10 @@ Set this property to define a placeholder string in the text input.
|
||||
|
||||
Set this property to a string that will be used for browser autocomplete.
|
||||
|
||||
**`rtl`**
|
||||
|
||||
Override the default text direction of the Admin panel for this field. Set to `true` to force right-to-left text direction.
|
||||
|
||||
### Example
|
||||
|
||||
`collections/ExampleCollection.ts`
|
||||
|
||||
@@ -52,6 +52,10 @@ Set this property to define a placeholder string in the textarea.
|
||||
|
||||
Set this property to a string that will be used for browser autocomplete.
|
||||
|
||||
**`rtl`**
|
||||
|
||||
Override the default text direction of the Admin panel for this field. Set to `true` to force right-to-left text direction.
|
||||
|
||||
### Example
|
||||
|
||||
`collections/ExampleCollection.ts`
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-use-before-define */
|
||||
/* eslint-disable no-use-before-define */
|
||||
import { IndexOptions, Schema, SchemaOptions, SchemaTypeOptions } from 'mongoose';
|
||||
import { SanitizedConfig } from 'payload/dist/config/types';
|
||||
import { SanitizedConfig, SanitizedLocalizationConfig } from 'payload/dist/config/types';
|
||||
import {
|
||||
ArrayField,
|
||||
Block,
|
||||
@@ -66,10 +66,10 @@ const formatBaseSchema = (field: FieldAffectingData, buildSchemaOptions: BuildSc
|
||||
return schema;
|
||||
};
|
||||
|
||||
const localizeSchema = (entity: NonPresentationalField | Tab, schema, localization) => {
|
||||
const localizeSchema = (entity: NonPresentationalField | Tab, schema, localization: false | SanitizedLocalizationConfig) => {
|
||||
if (fieldIsLocalized(entity) && localization && Array.isArray(localization.locales)) {
|
||||
return {
|
||||
type: localization.locales.reduce((localeSchema, locale) => ({
|
||||
type: localization.localeCodes.reduce((localeSchema, locale) => ({
|
||||
...localeSchema,
|
||||
[locale]: schema,
|
||||
}), {
|
||||
@@ -242,7 +242,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
|
||||
if (field.localized && config.localization) {
|
||||
schemaToReturn = {
|
||||
type: config.localization.locales.reduce((locales, locale) => {
|
||||
type: config.localization.localeCodes.reduce((locales, locale) => {
|
||||
let localeSchema: { [key: string]: any } = {};
|
||||
|
||||
if (hasManyRelations) {
|
||||
@@ -454,10 +454,10 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
});
|
||||
|
||||
if (field.localized && config.localization) {
|
||||
config.localization.locales.forEach((locale) => {
|
||||
config.localization.localeCodes.forEach((localeCode) => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore Possible incorrect typing in mongoose types, this works
|
||||
schema.path(`${field.name}.${locale}`).discriminator(blockItem.slug, blockSchema);
|
||||
schema.path(`${field.name}.${localeCode}`).discriminator(blockItem.slug, blockSchema);
|
||||
});
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { PaginateOptions } from 'mongoose';
|
||||
import { Config } from 'payload/dist/config/types';
|
||||
import { SanitizedConfig } from 'payload/dist/config/types';
|
||||
import { Field } from 'payload/dist/fields/config/types';
|
||||
import { getLocalizedSortProperty } from './getLocalizedSortProperty';
|
||||
|
||||
type Args = {
|
||||
sort: string
|
||||
config: Config
|
||||
config: SanitizedConfig
|
||||
fields: Field[]
|
||||
timestamps: boolean
|
||||
locale: string
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { sanitizeConfig } from 'payload/dist/config/sanitize';
|
||||
import { Config } from 'payload/dist/config/types';
|
||||
import { getLocalizedSortProperty } from './getLocalizedSortProperty';
|
||||
|
||||
@@ -11,7 +12,7 @@ describe('get localized sort property', () => {
|
||||
it('passes through a non-localized sort property', () => {
|
||||
const result = getLocalizedSortProperty({
|
||||
segments: ['title'],
|
||||
config,
|
||||
config: sanitizeConfig(config),
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
@@ -27,7 +28,7 @@ describe('get localized sort property', () => {
|
||||
it('properly localizes an un-localized sort property', () => {
|
||||
const result = getLocalizedSortProperty({
|
||||
segments: ['title'],
|
||||
config,
|
||||
config: sanitizeConfig(config),
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
@@ -44,7 +45,7 @@ describe('get localized sort property', () => {
|
||||
it('keeps specifically asked-for localized sort properties', () => {
|
||||
const result = getLocalizedSortProperty({
|
||||
segments: ['title', 'es'],
|
||||
config,
|
||||
config: sanitizeConfig(config),
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
@@ -61,7 +62,7 @@ describe('get localized sort property', () => {
|
||||
it('properly localizes nested sort properties', () => {
|
||||
const result = getLocalizedSortProperty({
|
||||
segments: ['group', 'title'],
|
||||
config,
|
||||
config: sanitizeConfig(config),
|
||||
fields: [
|
||||
{
|
||||
name: 'group',
|
||||
@@ -84,7 +85,7 @@ describe('get localized sort property', () => {
|
||||
it('keeps requested locale with nested sort properties', () => {
|
||||
const result = getLocalizedSortProperty({
|
||||
segments: ['group', 'title', 'es'],
|
||||
config,
|
||||
config: sanitizeConfig(config),
|
||||
fields: [
|
||||
{
|
||||
name: 'group',
|
||||
@@ -107,7 +108,7 @@ describe('get localized sort property', () => {
|
||||
it('properly localizes field within row', () => {
|
||||
const result = getLocalizedSortProperty({
|
||||
segments: ['title'],
|
||||
config,
|
||||
config: sanitizeConfig(config),
|
||||
fields: [
|
||||
{
|
||||
type: 'row',
|
||||
@@ -129,7 +130,7 @@ describe('get localized sort property', () => {
|
||||
it('properly localizes field within named tab', () => {
|
||||
const result = getLocalizedSortProperty({
|
||||
segments: ['tab', 'title'],
|
||||
config,
|
||||
config: sanitizeConfig(config),
|
||||
fields: [
|
||||
{
|
||||
type: 'tabs',
|
||||
@@ -156,7 +157,7 @@ describe('get localized sort property', () => {
|
||||
it('properly localizes field within unnamed tab', () => {
|
||||
const result = getLocalizedSortProperty({
|
||||
segments: ['title'],
|
||||
config,
|
||||
config: sanitizeConfig(config),
|
||||
fields: [
|
||||
{
|
||||
type: 'tabs',
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Config } from 'payload/dist/config/types';
|
||||
import { SanitizedConfig } from 'payload/dist/config/types';
|
||||
import { Field, fieldAffectsData, fieldIsPresentationalOnly } from 'payload/dist/fields/config/types';
|
||||
import flattenTopLevelFields from 'payload/dist/utilities/flattenTopLevelFields';
|
||||
|
||||
type Args = {
|
||||
segments: string[]
|
||||
config: Config
|
||||
config: SanitizedConfig
|
||||
fields: Field[]
|
||||
locale: string
|
||||
result?: string
|
||||
@@ -42,7 +42,7 @@ export const getLocalizedSortProperty = ({
|
||||
if (matchedField.localized) {
|
||||
// Check to see if next segment is a locale
|
||||
if (segments.length > 0) {
|
||||
const nextSegmentIsLocale = config.localization.locales.includes(remainingSegments[0]);
|
||||
const nextSegmentIsLocale = config.localization.localeCodes.includes(remainingSegments[0]);
|
||||
|
||||
// If next segment is locale, remove it from remaining segments
|
||||
// and use it to localize the current segment
|
||||
|
||||
@@ -30,7 +30,7 @@ const Routes: React.FC = () => {
|
||||
const [initialized, setInitialized] = useState(null);
|
||||
const { user, permissions, refreshCookie } = useAuth();
|
||||
const { i18n } = useTranslation();
|
||||
const locale = useLocale();
|
||||
const { code: locale } = useLocale();
|
||||
|
||||
const canAccessAdmin = permissions?.canAccessAdmin;
|
||||
|
||||
@@ -260,7 +260,7 @@ const Routes: React.FC = () => {
|
||||
const routesToReturn = [
|
||||
...globalRoutes,
|
||||
<Route
|
||||
key={`${global.slug}`}
|
||||
key={global.slug}
|
||||
path={`${match.url}/globals/${global.slug}`}
|
||||
exact
|
||||
>
|
||||
|
||||
@@ -19,7 +19,7 @@ const Autosave: React.FC<Props> = ({ collection, global, id, publishedDocUpdated
|
||||
const { versions, getVersions } = useDocumentInfo();
|
||||
const [fields] = useAllFormFields();
|
||||
const modified = useFormModified();
|
||||
const locale = useLocale();
|
||||
const { code: locale } = useLocale();
|
||||
const { replace } = useHistory();
|
||||
const { t, i18n } = useTranslation('version');
|
||||
|
||||
|
||||
@@ -129,6 +129,12 @@ a.btn {
|
||||
&--round {
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
[dir=rtl] &--icon {
|
||||
span{
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
&--icon {
|
||||
span {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
@import '../../../scss/styles';
|
||||
|
||||
.code-editor {
|
||||
direction: ltr;
|
||||
@include formInput;
|
||||
height: auto;
|
||||
padding: 0;
|
||||
|
||||
@@ -2,6 +2,14 @@
|
||||
|
||||
$cal-icon-width: 18px;
|
||||
|
||||
[dir=rtl] {
|
||||
.date-time-picker {
|
||||
.react-datepicker__input-container input {
|
||||
padding-right: base(3.4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.date-time-picker {
|
||||
|
||||
.react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box,
|
||||
|
||||
@@ -29,7 +29,7 @@ const Content: React.FC<DocumentDrawerProps> = ({
|
||||
}) => {
|
||||
const { serverURL, routes: { api } } = useConfig();
|
||||
const { toggleModal, modalState, closeModal } = useModal();
|
||||
const locale = useLocale();
|
||||
const { code: locale } = useLocale();
|
||||
const { permissions, user } = useAuth();
|
||||
const [internalState, setInternalState] = useState<Fields>();
|
||||
const { t, i18n } = useTranslation(['fields', 'general']);
|
||||
|
||||
@@ -91,6 +91,7 @@
|
||||
overflow: hidden;
|
||||
width: base(1);
|
||||
height: base(1);
|
||||
direction: ltr;
|
||||
|
||||
svg {
|
||||
width: base(2.75);
|
||||
|
||||
@@ -10,9 +10,9 @@ import { requests } from '../../../api';
|
||||
import { useForm, useFormModified } from '../../forms/Form/context';
|
||||
import MinimalTemplate from '../../templates/Minimal';
|
||||
import { getTranslation } from '../../../../utilities/getTranslation';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
|
||||
const baseClass = 'duplicate';
|
||||
|
||||
const Duplicate: React.FC<Props> = ({ slug, collection, id }) => {
|
||||
@@ -77,7 +77,7 @@ const Duplicate: React.FC<Props> = ({ slug, collection, id }) => {
|
||||
if (localization) {
|
||||
duplicateID = await create(localization.defaultLocale);
|
||||
let abort = false;
|
||||
localization.locales
|
||||
localization.localeCodes
|
||||
.filter((locale) => locale !== localization.defaultLocale)
|
||||
.forEach(async (locale) => {
|
||||
if (!abort) {
|
||||
|
||||
@@ -67,7 +67,13 @@
|
||||
width: base(2.75);
|
||||
height: base(2.75);
|
||||
position: relative;
|
||||
left: base(-.825);
|
||||
[dir=ltr] & {
|
||||
left: base(-.825);
|
||||
}
|
||||
|
||||
[dir=rtl] & {
|
||||
right: base(-.825);
|
||||
}
|
||||
top: base(-.825);
|
||||
|
||||
.stroke {
|
||||
@@ -83,7 +89,12 @@
|
||||
padding-bottom: base(2);
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
[dir=rtl] &__sidebar-wrap {
|
||||
left: 0;
|
||||
border-right: 1px solid var(--theme-elevation-100);
|
||||
right:auto
|
||||
}
|
||||
|
||||
&__sidebar-wrap {
|
||||
position: fixed;
|
||||
width: base(15);
|
||||
@@ -109,7 +120,12 @@
|
||||
&__collection-actions,
|
||||
&__meta,
|
||||
&__sidebar-fields {
|
||||
padding-left: base(1.5);
|
||||
[dir=ltr] & {
|
||||
padding-left: base(1.5);
|
||||
}
|
||||
[dir=rtl] & {
|
||||
padding-right: base(1.5);
|
||||
}
|
||||
}
|
||||
|
||||
&__document-actions {
|
||||
|
||||
@@ -17,19 +17,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
&__buttons {
|
||||
margin-left: $baseline;
|
||||
}
|
||||
|
||||
&__buttons-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: base(.5);
|
||||
gap: base(.5);
|
||||
|
||||
.btn, .pill {
|
||||
margin: 0 0 0 base(.5);
|
||||
[dir=ltr] & {
|
||||
margin-right: base(.5);
|
||||
}
|
||||
|
||||
[dir=rtl] & {
|
||||
margin-left: base(.5);
|
||||
}
|
||||
|
||||
.btn {
|
||||
background-color: var(--theme-elevation-100);
|
||||
cursor: pointer;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
@include mid-break {
|
||||
margin-top: base(1.5);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,9 +23,8 @@
|
||||
|
||||
button .pill {
|
||||
pointer-events: none;
|
||||
margin: 0;
|
||||
margin-top: base(0.25);
|
||||
margin-left: base(0.5);
|
||||
margin-right: base(0.5);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
|
||||
.localizer {
|
||||
position: relative;
|
||||
|
||||
padding: base(.125) base(1.5) base(.125) 0;
|
||||
[dir=rtl] & {
|
||||
padding: base(.125) 0 base(.125) base(1.5);
|
||||
}
|
||||
|
||||
button {
|
||||
color: currentColor;
|
||||
padding: base(.25) 0;
|
||||
|
||||
@@ -7,12 +7,15 @@ import { useLocale } from '../../utilities/Locale';
|
||||
import { useSearchParams } from '../../utilities/SearchParams';
|
||||
import Popup from '../Popup';
|
||||
|
||||
|
||||
import './index.scss';
|
||||
|
||||
const baseClass = 'localizer';
|
||||
|
||||
const Localizer: React.FC = () => {
|
||||
const { localization } = useConfig();
|
||||
const config = useConfig();
|
||||
const { localization } = config;
|
||||
|
||||
const locale = useLocale();
|
||||
const searchParams = useSearchParams();
|
||||
const { t } = useTranslation('general');
|
||||
@@ -25,7 +28,7 @@ const Localizer: React.FC = () => {
|
||||
<Popup
|
||||
showScrollbar
|
||||
horizontalAlign="left"
|
||||
button={locale}
|
||||
button={locale.label}
|
||||
render={({ close }) => (
|
||||
<div>
|
||||
<span>{t('locales')}</span>
|
||||
@@ -35,27 +38,27 @@ const Localizer: React.FC = () => {
|
||||
|
||||
const localeClasses = [
|
||||
baseLocaleClass,
|
||||
locale === localeOption && `${baseLocaleClass}--active`,
|
||||
locale.code === localeOption.code && `${baseLocaleClass}--active`,
|
||||
].filter(Boolean).join('');
|
||||
|
||||
const newParams = {
|
||||
...searchParams,
|
||||
locale: localeOption,
|
||||
locale: localeOption.code,
|
||||
};
|
||||
|
||||
const search = qs.stringify(newParams);
|
||||
|
||||
if (localeOption !== locale) {
|
||||
if (localeOption.code !== locale.code) {
|
||||
return (
|
||||
<li
|
||||
key={localeOption}
|
||||
key={localeOption.code}
|
||||
className={localeClasses}
|
||||
>
|
||||
<Link
|
||||
to={{ search }}
|
||||
onClick={close}
|
||||
>
|
||||
{localeOption}
|
||||
{localeOption.label}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
|
||||
@@ -8,7 +8,12 @@
|
||||
height: 100vh;
|
||||
width: var(--nav-width);
|
||||
overflow: hidden;
|
||||
border-right: 1px solid var(--theme-elevation-100);
|
||||
[dir=ltr] & {
|
||||
border-right: 1px solid var(--theme-elevation-100);
|
||||
}
|
||||
[dir=rtl] & {
|
||||
border-left: 1px solid var(--theme-elevation-100);
|
||||
}
|
||||
|
||||
header {
|
||||
width: 100%;
|
||||
@@ -27,7 +32,12 @@
|
||||
}
|
||||
|
||||
&__brand {
|
||||
margin-right: base(1);
|
||||
[dir=ltr] & {
|
||||
margin-right: base(1);
|
||||
}
|
||||
[dir=rtl] & {
|
||||
margin-left: base(1);
|
||||
}
|
||||
}
|
||||
|
||||
&__mobile-menu-btn {
|
||||
@@ -91,6 +101,9 @@
|
||||
padding: base(.125) base(1.5) base(.125) 0;
|
||||
display: flex;
|
||||
text-decoration: none;
|
||||
[dir=rtl] & {
|
||||
padding: base(.125) 0 base(.125) base(1.5);
|
||||
}
|
||||
|
||||
&:focus:not(:focus-visible) {
|
||||
box-shadow: none;
|
||||
@@ -103,8 +116,13 @@
|
||||
|
||||
&.active {
|
||||
font-weight: normal;
|
||||
padding-left: base(.6);
|
||||
font-weight: 600;
|
||||
[dir=ltr] & {
|
||||
padding-left: base(.6);
|
||||
}
|
||||
[dir=rtl] & {
|
||||
padding-right: base(.6);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -113,8 +131,14 @@
|
||||
svg {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
left: - base(.5);
|
||||
transform: rotate(-90deg);
|
||||
[dir=ltr] & {
|
||||
left: - base(.5);
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
[dir=rtl] & {
|
||||
right: - base(.5);
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
|
||||
@@ -8,14 +8,22 @@
|
||||
cursor: pointer;
|
||||
color: var(--theme-elevation-400);
|
||||
background: transparent;
|
||||
padding-left: 0;
|
||||
border: 0;
|
||||
margin-top: base(.25);
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding-right: base(.5);
|
||||
[dir=ltr] & {
|
||||
padding-right: base(.5);
|
||||
padding-left: 0;
|
||||
align-items: flex-start;
|
||||
text-align: left;
|
||||
}
|
||||
[dir=rtl] & {
|
||||
padding-left: base(.5);
|
||||
padding-right: 0;
|
||||
align-items: flex-start;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
svg {
|
||||
flex-shrink: 0;
|
||||
@@ -36,7 +44,12 @@
|
||||
}
|
||||
|
||||
&__indicator {
|
||||
margin-left: auto;
|
||||
[dir=ltr] & {
|
||||
margin-left: auto;
|
||||
}
|
||||
[dir=rtl] & {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.stroke {
|
||||
stroke: var(--theme-elevation-200);
|
||||
|
||||
@@ -47,6 +47,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
[dir=rtl] &--align-icon-left {
|
||||
padding-right: 0;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
[dir=rtl] &--align-icon-right {
|
||||
padding-right: 10px;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
&--align-icon-left {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
@@ -51,7 +51,12 @@
|
||||
|
||||
&--size-small {
|
||||
.popup__scroll {
|
||||
padding: base(.75) calc(var(--scrollbar-width) + #{base(.75)}) base(.75) base(.75);
|
||||
[dir=ltr] & {
|
||||
padding: base(.75) calc(var(--scrollbar-width) + #{base(.75)}) base(.75) base(.75);
|
||||
}
|
||||
[dir=rtl] & {
|
||||
padding: base(.75) base(.75) base(.75) calc(var(--scrollbar-width) + #{base(.75)});
|
||||
}
|
||||
}
|
||||
|
||||
.popup__content {
|
||||
@@ -133,9 +138,14 @@
|
||||
&--h-align-right {
|
||||
.popup__content {
|
||||
right: - base(1.75);
|
||||
|
||||
[dir=rtl] & {
|
||||
right: - base(0.75);
|
||||
}
|
||||
&:after {
|
||||
right: base(1.75);
|
||||
[dir=rtl] & {
|
||||
right: base(0.75);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ const PreviewButton: React.FC<Props> = ({
|
||||
const { id, collection, global } = useDocumentInfo();
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const locale = useLocale();
|
||||
const { code: locale } = useLocale();
|
||||
const { token } = useAuth();
|
||||
const { serverURL, routes: { api } } = useConfig();
|
||||
const { t } = useTranslation('version');
|
||||
|
||||
@@ -58,7 +58,7 @@ export const SaveDraft: React.FC<Props> = ({ CustomComponent }) => {
|
||||
const { submit } = useForm();
|
||||
const { collection, global, id } = useDocumentInfo();
|
||||
const modified = useFormModified();
|
||||
const locale = useLocale();
|
||||
const { code: locale } = useLocale();
|
||||
const { t } = useTranslation('version');
|
||||
|
||||
const canSaveDraft = modified;
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
@import '../../../scss/styles';
|
||||
|
||||
[dir=rtl] .search-filter{
|
||||
svg {
|
||||
right: base(.5);
|
||||
left:0
|
||||
}
|
||||
&__input {
|
||||
padding-right: base(2);
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
.search-filter {
|
||||
position: relative;
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ const Status: React.FC = () => {
|
||||
} = useConfig();
|
||||
const [processing, setProcessing] = useState(false);
|
||||
const { reset: resetForm } = useForm();
|
||||
const locale = useLocale();
|
||||
const { code: locale } = useLocale();
|
||||
const { t, i18n } = useTranslation('version');
|
||||
|
||||
const unPublishModalSlug = `confirm-un-publish-${id}`;
|
||||
|
||||
@@ -9,7 +9,12 @@
|
||||
}
|
||||
|
||||
a {
|
||||
margin-right: base(.25);
|
||||
[dir=ltr] & {
|
||||
margin-right: base(.25);
|
||||
}
|
||||
[dir=rtl] & {
|
||||
margin-left: base(.25);
|
||||
}
|
||||
border: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -17,8 +22,14 @@
|
||||
text-decoration: none;
|
||||
|
||||
svg {
|
||||
margin-left: base(.25);
|
||||
transform: rotate(-90deg);
|
||||
[dir=ltr] & {
|
||||
margin-left: base(.25);
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
[dir=rtl] & {
|
||||
margin-right: base(.25);
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
th {
|
||||
font-weight: normal;
|
||||
text-align: left;
|
||||
[dir = rtl] & {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
[dir=rtl] &__field,
|
||||
&__operator {
|
||||
margin-left: $baseline;
|
||||
margin-right:0;
|
||||
}
|
||||
|
||||
&__field,
|
||||
&__operator {
|
||||
margin-right: $baseline;
|
||||
@@ -27,6 +33,9 @@
|
||||
.btn {
|
||||
vertical-align: middle;
|
||||
margin: 0 0 0 $baseline;
|
||||
[dir=rtl] & {
|
||||
margin: 0 $baseline 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
@include mid-break {
|
||||
|
||||
@@ -51,7 +51,7 @@ const Form: React.FC<Props> = (props) => {
|
||||
} = props;
|
||||
|
||||
const history = useHistory();
|
||||
const locale = useLocale();
|
||||
const { code: locale } = useLocale();
|
||||
const { t, i18n } = useTranslation('general');
|
||||
const { refreshCookie, user } = useAuth();
|
||||
const { id, getDocPreferences, collection, global } = useDocumentInfo();
|
||||
|
||||
@@ -9,7 +9,13 @@ label.field-label {
|
||||
|
||||
.required {
|
||||
color: var(--theme-error-500);
|
||||
margin-left: base(.25);
|
||||
margin-right: auto;
|
||||
[dir=ltr] & {
|
||||
margin-left: base(.25);
|
||||
margin-right: auto;
|
||||
}
|
||||
[dir=rtl] & {
|
||||
margin-right: base(.25);
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ type NullifyLocaleFieldProps = {
|
||||
}
|
||||
export const NullifyLocaleField: React.FC<NullifyLocaleFieldProps> = ({ localized, path, fieldValue }) => {
|
||||
const { dispatchFields, setModified } = useForm();
|
||||
const currentLocale = useLocale();
|
||||
const { code: currentLocale } = useLocale();
|
||||
const { localization } = useConfig();
|
||||
const [checked, setChecked] = React.useState<boolean>(typeof fieldValue !== 'number');
|
||||
const defaultLocale = (localization && localization.defaultLocale) ? localization.defaultLocale : 'en';
|
||||
|
||||
@@ -31,11 +31,12 @@
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&__header-actions {
|
||||
list-style: none;
|
||||
margin: 0 0 0 auto;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ const ArrayFieldType: React.FC<Props> = (props) => {
|
||||
const { setDocFieldPreferences } = useDocumentInfo();
|
||||
const { dispatchFields, setModified, addFieldRow, removeFieldRow } = useForm();
|
||||
const submitted = useFormSubmitted();
|
||||
const locale = useLocale();
|
||||
const { code: locale } = useLocale();
|
||||
const { t, i18n } = useTranslation('fields');
|
||||
const { localization } = useConfig();
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&__heading-with-error {
|
||||
@@ -41,7 +42,7 @@
|
||||
|
||||
&__header-actions {
|
||||
list-style: none;
|
||||
margin: 0 0 0 auto;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
}
|
||||
@@ -73,10 +74,6 @@
|
||||
line-height: unset;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__row {
|
||||
margin-bottom: base(.5);
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ const BlocksField: React.FC<Props> = (props) => {
|
||||
|
||||
const { setDocFieldPreferences } = useDocumentInfo();
|
||||
const { dispatchFields, setModified, addFieldRow, removeFieldRow } = useForm();
|
||||
const locale = useLocale();
|
||||
const { code: locale } = useLocale();
|
||||
const { localization } = useConfig();
|
||||
const drawerSlug = useDrawerSlug('blocks-drawer');
|
||||
const submitted = useFormSubmitted();
|
||||
|
||||
@@ -33,6 +33,10 @@
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
[dir=rtl] &__input {
|
||||
margin-right: 0;
|
||||
margin-left: base(.5);
|
||||
}
|
||||
|
||||
&__input {
|
||||
// visible checkbox
|
||||
|
||||
@@ -36,6 +36,11 @@
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
[dir=rtl] &__label {
|
||||
margin-left: 0;
|
||||
margin-right: base(.5);
|
||||
}
|
||||
|
||||
&__label {
|
||||
margin-left: base(.5);
|
||||
|
||||
@@ -14,8 +14,13 @@
|
||||
}
|
||||
|
||||
li {
|
||||
padding-right: $baseline;
|
||||
flex-shrink: 0;
|
||||
[dir=ltr] & {
|
||||
padding-right: $baseline;
|
||||
}
|
||||
[dir=rtl] & {
|
||||
padding-left: $baseline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ const Relationship: React.FC<Props> = (props) => {
|
||||
|
||||
const { t, i18n } = useTranslation('fields');
|
||||
const { permissions } = useAuth();
|
||||
const locale = useLocale();
|
||||
const { code: locale } = useLocale();
|
||||
const formProcessing = useFormProcessing();
|
||||
const hasMultipleRelations = Array.isArray(relationTo);
|
||||
const [options, dispatchOptions] = useReducer(optionsReducer, []);
|
||||
|
||||
@@ -62,7 +62,7 @@ export const LinkButton: React.FC<{
|
||||
}> = ({ fieldProps }) => {
|
||||
const customFieldSchema = fieldProps?.admin?.link?.fields;
|
||||
const { user } = useAuth();
|
||||
const locale = useLocale();
|
||||
const { code: locale } = useLocale();
|
||||
const [initialState, setInitialState] = useState<Fields>({});
|
||||
|
||||
const { t, i18n } = useTranslation(['upload', 'general']);
|
||||
|
||||
@@ -76,7 +76,7 @@ export const LinkElement: React.FC<{
|
||||
const editor = useSlate();
|
||||
const config = useConfig();
|
||||
const { user } = useAuth();
|
||||
const locale = useLocale();
|
||||
const { code: locale } = useLocale();
|
||||
const { t, i18n } = useTranslation('fields');
|
||||
const { openModal, toggleModal, closeModal } = useModal();
|
||||
const [renderModal, setRenderModal] = useState(false);
|
||||
|
||||
@@ -31,7 +31,7 @@ export const UploadDrawer: React.FC<ElementProps & {
|
||||
} = props;
|
||||
|
||||
const { t, i18n } = useTranslation();
|
||||
const locale = useLocale();
|
||||
const { code: locale } = useLocale();
|
||||
const { user } = useAuth();
|
||||
const { closeModal } = useModal();
|
||||
const { getDocPreferences } = useDocumentInfo();
|
||||
|
||||
@@ -10,21 +10,22 @@ import { getTranslation } from '../../../../../utilities/getTranslation';
|
||||
import './index.scss';
|
||||
|
||||
export type TextInputProps = Omit<TextField, 'type'> & {
|
||||
showError?: boolean
|
||||
errorMessage?: string
|
||||
readOnly?: boolean
|
||||
path: string
|
||||
required?: boolean
|
||||
value?: string
|
||||
description?: Description
|
||||
onChange?: (e: ChangeEvent<HTMLInputElement>) => void
|
||||
onKeyDown?: React.KeyboardEventHandler<HTMLInputElement>
|
||||
placeholder?: Record<string, string> | string
|
||||
style?: React.CSSProperties
|
||||
className?: string
|
||||
width?: string
|
||||
inputRef?: React.MutableRefObject<HTMLInputElement>
|
||||
}
|
||||
showError?: boolean;
|
||||
errorMessage?: string;
|
||||
readOnly?: boolean;
|
||||
path: string;
|
||||
required?: boolean;
|
||||
value?: string;
|
||||
description?: Description;
|
||||
onChange?: (e: ChangeEvent<HTMLInputElement>) => void;
|
||||
onKeyDown?: React.KeyboardEventHandler<HTMLInputElement>;
|
||||
placeholder?: Record<string, string> | string;
|
||||
style?: React.CSSProperties;
|
||||
className?: string;
|
||||
width?: string;
|
||||
inputRef?: React.MutableRefObject<HTMLInputElement>;
|
||||
rtl?: boolean;
|
||||
};
|
||||
|
||||
const TextInput: React.FC<TextInputProps> = (props) => {
|
||||
const {
|
||||
@@ -43,6 +44,7 @@ const TextInput: React.FC<TextInputProps> = (props) => {
|
||||
className,
|
||||
width,
|
||||
inputRef,
|
||||
rtl,
|
||||
} = props;
|
||||
|
||||
const { i18n } = useTranslation();
|
||||
@@ -82,6 +84,7 @@ const TextInput: React.FC<TextInputProps> = (props) => {
|
||||
placeholder={getTranslation(placeholder, i18n)}
|
||||
type="text"
|
||||
name={path}
|
||||
data-rtl={rtl}
|
||||
/>
|
||||
<FieldDescription
|
||||
className={`field-description-${path.replace(/\./gi, '__')}`}
|
||||
|
||||
@@ -27,4 +27,4 @@ html[data-theme=dark] {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,9 @@ import withCondition from '../../withCondition';
|
||||
import { text } from '../../../../../fields/validations';
|
||||
import { Props } from './types';
|
||||
import TextInput from './Input';
|
||||
import { useLocale } from '../../../utilities/Locale';
|
||||
import { useConfig } from '../../../utilities/Config';
|
||||
import { isFieldRTL } from '../shared';
|
||||
|
||||
const Text: React.FC<Props> = (props) => {
|
||||
const {
|
||||
@@ -14,6 +17,7 @@ const Text: React.FC<Props> = (props) => {
|
||||
label,
|
||||
minLength,
|
||||
maxLength,
|
||||
localized,
|
||||
admin: {
|
||||
placeholder,
|
||||
readOnly,
|
||||
@@ -22,11 +26,22 @@ const Text: React.FC<Props> = (props) => {
|
||||
width,
|
||||
description,
|
||||
condition,
|
||||
rtl,
|
||||
} = {},
|
||||
inputRef,
|
||||
} = props;
|
||||
|
||||
const path = pathFromProps || name;
|
||||
const locale = useLocale();
|
||||
|
||||
const { localization } = useConfig();
|
||||
const isRTL = isFieldRTL({
|
||||
fieldRTL: rtl,
|
||||
fieldLocalized: localized,
|
||||
locale,
|
||||
localizationConfig: localization || undefined,
|
||||
});
|
||||
|
||||
|
||||
const memoizedValidate = useCallback((value, options) => {
|
||||
return validate(value, { ...options, minLength, maxLength, required });
|
||||
@@ -62,6 +77,7 @@ const Text: React.FC<Props> = (props) => {
|
||||
width={width}
|
||||
description={description}
|
||||
inputRef={inputRef}
|
||||
rtl={isRTL}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -23,6 +23,7 @@ export type TextAreaInputProps = Omit<TextareaField, 'type'> & {
|
||||
className?: string
|
||||
width?: string
|
||||
rows?: number
|
||||
rtl?: boolean;
|
||||
}
|
||||
|
||||
const TextareaInput: React.FC<TextAreaInputProps> = (props) => {
|
||||
@@ -41,6 +42,7 @@ const TextareaInput: React.FC<TextAreaInputProps> = (props) => {
|
||||
errorMessage,
|
||||
onChange,
|
||||
rows,
|
||||
rtl,
|
||||
} = props;
|
||||
|
||||
const { i18n } = useTranslation();
|
||||
@@ -88,6 +90,7 @@ const TextareaInput: React.FC<TextAreaInputProps> = (props) => {
|
||||
placeholder={getTranslation(placeholder, i18n)}
|
||||
name={path}
|
||||
rows={rows}
|
||||
data-rtl={rtl}
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
@@ -64,6 +64,9 @@
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
&[data-rtl='true'] {
|
||||
direction: rtl;
|
||||
}
|
||||
}
|
||||
|
||||
// Clone of textarea with same height
|
||||
|
||||
@@ -6,8 +6,11 @@ import { textarea } from '../../../../../fields/validations';
|
||||
import { Props } from './types';
|
||||
import TextareaInput from './Input';
|
||||
import { getTranslation } from '../../../../../utilities/getTranslation';
|
||||
import { useLocale } from '../../../utilities/Locale';
|
||||
|
||||
import './index.scss';
|
||||
import { useConfig } from '../../../utilities/Config';
|
||||
import { isFieldRTL } from '../shared';
|
||||
|
||||
const Textarea: React.FC<Props> = (props) => {
|
||||
const {
|
||||
@@ -17,6 +20,7 @@ const Textarea: React.FC<Props> = (props) => {
|
||||
validate = textarea,
|
||||
maxLength,
|
||||
minLength,
|
||||
localized,
|
||||
admin: {
|
||||
readOnly,
|
||||
style,
|
||||
@@ -26,6 +30,7 @@ const Textarea: React.FC<Props> = (props) => {
|
||||
rows,
|
||||
description,
|
||||
condition,
|
||||
rtl,
|
||||
} = {},
|
||||
label,
|
||||
} = props;
|
||||
@@ -34,6 +39,15 @@ const Textarea: React.FC<Props> = (props) => {
|
||||
|
||||
const path = pathFromProps || name;
|
||||
|
||||
const locale = useLocale();
|
||||
|
||||
const { localization } = useConfig();
|
||||
const isRTL = isFieldRTL({
|
||||
fieldRTL: rtl,
|
||||
fieldLocalized: localized,
|
||||
locale,
|
||||
localizationConfig: localization || undefined,
|
||||
});
|
||||
const memoizedValidate = useCallback((value, options) => {
|
||||
return validate(value, { ...options, required, maxLength, minLength });
|
||||
}, [validate, required, maxLength, minLength]);
|
||||
@@ -68,6 +82,7 @@ const Textarea: React.FC<Props> = (props) => {
|
||||
width={width}
|
||||
description={description}
|
||||
rows={rows}
|
||||
rtl={isRTL}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
32
src/admin/components/forms/field-types/shared.ts
Normal file
32
src/admin/components/forms/field-types/shared.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { Locale, SanitizedLocalizationConfig } from '../../../../config/types';
|
||||
|
||||
/**
|
||||
* Determines whether a field should be displayed as right-to-left (RTL) based on its configuration, payload's localization configuration and the adming user's currently enabled locale.
|
||||
|
||||
* @returns Whether the field should be displayed as RTL.
|
||||
*/
|
||||
export function isFieldRTL({
|
||||
fieldRTL,
|
||||
fieldLocalized,
|
||||
locale,
|
||||
localizationConfig,
|
||||
}: {
|
||||
fieldRTL: boolean;
|
||||
fieldLocalized: boolean;
|
||||
locale: Locale;
|
||||
localizationConfig?: SanitizedLocalizationConfig;
|
||||
}) {
|
||||
const hasMultipleLocales = locale
|
||||
&& (localizationConfig)
|
||||
&& (localizationConfig.locales)
|
||||
&& (localizationConfig.locales.length > 1);
|
||||
const isCurrentLocaleDefaultLocale = locale?.code === localizationConfig?.defaultLocale;
|
||||
|
||||
return ((fieldRTL !== false && locale?.rtl === true)
|
||||
&& (
|
||||
fieldLocalized
|
||||
|| (!fieldLocalized && !hasMultipleLocales) // If there is only one locale which is also rtl, that field is rtl too
|
||||
|| (!fieldLocalized && isCurrentLocaleDefaultLocale) // If the current locale is the default locale, but the field is not localized, that field is rtl too
|
||||
))
|
||||
|| (fieldRTL === true); // If fieldRTL is true. This should be useful for when no localization is set at all in the payload config, but you still want fields to be rtl.
|
||||
}
|
||||
@@ -15,6 +15,11 @@
|
||||
}
|
||||
|
||||
.btn {
|
||||
margin-right: $baseline;
|
||||
[dir=ltr] & {
|
||||
margin-right: $baseline;
|
||||
}
|
||||
[dir=rtl] & {
|
||||
margin-left: $baseline;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,12 @@
|
||||
@include mid-break {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-left: 0;
|
||||
[dir=ltr] & {
|
||||
margin-left: 0;
|
||||
}
|
||||
[dir=rtl] & {
|
||||
margin-right: 0;
|
||||
}
|
||||
padding-top: base(3);
|
||||
|
||||
&__wrap {
|
||||
|
||||
@@ -16,7 +16,10 @@ export const I18n: React.FC = () => {
|
||||
}
|
||||
|
||||
i18n
|
||||
.use(LanguageDetector)
|
||||
.use(new LanguageDetector(null, {
|
||||
lookupCookie: 'lng',
|
||||
lookupLocalStorage: 'lng',
|
||||
}))
|
||||
.use(initReactI18next)
|
||||
.init(deepmerge(defaultOptions, config.i18n || {}));
|
||||
loader.config({ 'vs/nls': { availableLanguages: { '*': getSupportedMonacoLocale(i18n.language) } } });
|
||||
|
||||
@@ -1,19 +1,29 @@
|
||||
import React, {
|
||||
createContext, useContext, useState, useEffect,
|
||||
} from 'react';
|
||||
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||
import { Locale } from '../../../../config/types';
|
||||
import { useConfig } from '../Config';
|
||||
import { useAuth } from '../Auth';
|
||||
import { usePreferences } from '../Preferences';
|
||||
import { useSearchParams } from '../SearchParams';
|
||||
import { findLocaleFromCode } from '../../../../utilities/findLocaleFromCode';
|
||||
|
||||
const Context = createContext('');
|
||||
const LocaleContext = createContext(null);
|
||||
|
||||
export const LocaleProvider: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
|
||||
export const LocaleProvider: React.FC<{ children?: React.ReactNode }> = ({
|
||||
children,
|
||||
}) => {
|
||||
const { localization } = useConfig();
|
||||
|
||||
const { user } = useAuth();
|
||||
const defaultLocale = (localization && localization.defaultLocale) ? localization.defaultLocale : 'en';
|
||||
const defaultLocale = (localization && localization.defaultLocale)
|
||||
? localization.defaultLocale
|
||||
: 'en';
|
||||
const searchParams = useSearchParams();
|
||||
const [locale, setLocale] = useState<string>(searchParams?.locale as string || defaultLocale);
|
||||
const [localeCode, setLocaleCode] = useState<string>(
|
||||
(searchParams?.locale as string) || defaultLocale,
|
||||
);
|
||||
const [locale, setLocale] = useState<Locale | null>(
|
||||
localization && findLocaleFromCode(localization, localeCode),
|
||||
);
|
||||
const { getPreference, setPreference } = usePreferences();
|
||||
const localeFromParams = searchParams.locale;
|
||||
|
||||
@@ -23,8 +33,14 @@ export const LocaleProvider: React.FC<{ children?: React.ReactNode }> = ({ child
|
||||
}
|
||||
|
||||
// set locale from search param
|
||||
if (localeFromParams && localization.locales.indexOf(localeFromParams as string) > -1) {
|
||||
setLocale(localeFromParams as string);
|
||||
if (
|
||||
localeFromParams
|
||||
&& localization.localeCodes.indexOf(localeFromParams as string) > -1
|
||||
) {
|
||||
setLocaleCode(localeFromParams as string);
|
||||
setLocale(
|
||||
findLocaleFromCode(localization, localeFromParams as string),
|
||||
);
|
||||
if (user) setPreference('locale', localeFromParams);
|
||||
return;
|
||||
}
|
||||
@@ -35,24 +51,38 @@ export const LocaleProvider: React.FC<{ children?: React.ReactNode }> = ({ child
|
||||
let isPreferenceInConfig: boolean;
|
||||
if (user) {
|
||||
preferenceLocale = await getPreference<string>('locale');
|
||||
isPreferenceInConfig = preferenceLocale && (localization.locales.indexOf(preferenceLocale) > -1);
|
||||
isPreferenceInConfig = preferenceLocale
|
||||
&& localization.localeCodes.indexOf(preferenceLocale) > -1;
|
||||
if (isPreferenceInConfig) {
|
||||
setLocale(preferenceLocale);
|
||||
setLocaleCode(preferenceLocale);
|
||||
setLocale(
|
||||
findLocaleFromCode(localization, preferenceLocale as string),
|
||||
);
|
||||
return;
|
||||
}
|
||||
setPreference('locale', defaultLocale);
|
||||
}
|
||||
setLocale(defaultLocale);
|
||||
setLocaleCode(defaultLocale);
|
||||
setLocale(findLocaleFromCode(localization, defaultLocale));
|
||||
})();
|
||||
}, [defaultLocale, getPreference, localeFromParams, localization, setPreference, user]);
|
||||
}, [
|
||||
defaultLocale,
|
||||
getPreference,
|
||||
localeFromParams,
|
||||
setPreference,
|
||||
user,
|
||||
localization,
|
||||
]);
|
||||
|
||||
return (
|
||||
<Context.Provider value={locale}>
|
||||
<LocaleContext.Provider value={locale}>
|
||||
{children}
|
||||
</Context.Provider>
|
||||
</LocaleContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useLocale = (): string => useContext(Context);
|
||||
|
||||
export default Context;
|
||||
/**
|
||||
* A hook that returns the current locale object.
|
||||
*/
|
||||
export const useLocale = (): Locale | null => useContext(LocaleContext);
|
||||
export default LocaleContext;
|
||||
|
||||
@@ -1,18 +1,37 @@
|
||||
import React from 'react';
|
||||
import Helmet from 'react-helmet';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useConfig } from '../Config';
|
||||
import { Props } from './types';
|
||||
import payloadFavicon from '../../../assets/images/favicon.svg';
|
||||
import payloadOgImage from '../../../assets/images/og-image.png';
|
||||
import useMountEffect from '../../../hooks/useMountEffect';
|
||||
|
||||
const rtlLanguages = [
|
||||
'ar',
|
||||
'fa',
|
||||
'ha',
|
||||
'ku',
|
||||
'ur',
|
||||
'ps',
|
||||
'dv',
|
||||
'ks',
|
||||
'khw',
|
||||
'he',
|
||||
'yi',
|
||||
];
|
||||
|
||||
const Meta: React.FC<Props> = ({
|
||||
description,
|
||||
lang = 'en',
|
||||
// lang = 'en',
|
||||
meta = [],
|
||||
title,
|
||||
keywords = 'CMS, Admin, Dashboard',
|
||||
}) => {
|
||||
const { i18n } = useTranslation();
|
||||
const currentLanguage = i18n.language;
|
||||
const currentDirection = rtlLanguages.includes(currentLanguage) ? 'RTL' : 'LTR';
|
||||
|
||||
const config = useConfig();
|
||||
const titleSuffix = config.admin.meta?.titleSuffix ?? '- Payload';
|
||||
const favicon = config.admin.meta.favicon ?? payloadFavicon;
|
||||
@@ -28,7 +47,8 @@ const Meta: React.FC<Props> = ({
|
||||
return (
|
||||
<Helmet
|
||||
htmlAttributes={{
|
||||
lang,
|
||||
lang: currentLanguage,
|
||||
dir: currentDirection,
|
||||
}}
|
||||
title={`${title} ${titleSuffix}`}
|
||||
meta={[
|
||||
|
||||
@@ -29,7 +29,12 @@
|
||||
display: flex;
|
||||
|
||||
li {
|
||||
margin-right: base(.75);
|
||||
[dir=ltr] & {
|
||||
margin-right: base(.75);
|
||||
}
|
||||
[dir=rtl] & {
|
||||
margin-left: base(.75);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,10 +47,17 @@
|
||||
position: fixed;
|
||||
width: base(15);
|
||||
height: 100%;
|
||||
top: 0;
|
||||
right: 0;
|
||||
overflow: visible;
|
||||
border-left: 1px solid var(--theme-elevation-100);
|
||||
[dir=ltr] & {
|
||||
top: 0;
|
||||
right: 0;
|
||||
border-left: 1px solid var(--theme-elevation-100);
|
||||
}
|
||||
[dir=rtl] & {
|
||||
top: 0;
|
||||
left: 0;
|
||||
border-right: 1px solid var(--theme-elevation-100);
|
||||
}
|
||||
}
|
||||
|
||||
&__sidebar {
|
||||
@@ -64,7 +76,12 @@
|
||||
&__document-actions,
|
||||
&__meta,
|
||||
&__sidebar-fields {
|
||||
padding-left: base(1.5);
|
||||
[dir=ltr] & {
|
||||
padding-left: base(1.5);
|
||||
}
|
||||
[dir=rtl] & {
|
||||
padding-right: base(1.5);
|
||||
}
|
||||
}
|
||||
|
||||
&__document-actions {
|
||||
@@ -88,11 +105,21 @@
|
||||
}
|
||||
|
||||
>*:first-child {
|
||||
margin-right: base(.5);
|
||||
[dir=ltr] & {
|
||||
margin-right: base(.5);
|
||||
}
|
||||
[dir=rtl] & {
|
||||
margin-left: base(.5);
|
||||
}
|
||||
}
|
||||
|
||||
>*:last-child {
|
||||
margin-left: base(.5);
|
||||
[dir=ltr] & {
|
||||
margin-left: base(.5);
|
||||
}
|
||||
[dir=rtl] & {
|
||||
margin-right: base(.5);
|
||||
}
|
||||
}
|
||||
|
||||
.form-submit {
|
||||
@@ -105,8 +132,14 @@
|
||||
}
|
||||
|
||||
&__api-url {
|
||||
margin-bottom: base(1.5);
|
||||
padding-right: base(1.5);
|
||||
[dir=ltr] & {
|
||||
margin-bottom: base(1.5);
|
||||
padding-right: base(1.5);
|
||||
}
|
||||
[dir=rtl] & {
|
||||
margin-bottom: base(1.5);
|
||||
padding-left: base(1.5);
|
||||
}
|
||||
|
||||
a {
|
||||
display: block;
|
||||
@@ -176,10 +209,18 @@
|
||||
|
||||
&__document-actions {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: auto;
|
||||
[dir=ltr] & {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: auto;
|
||||
}
|
||||
[dir=rtl] & {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&__meta {
|
||||
@@ -191,7 +232,12 @@
|
||||
&__document-actions,
|
||||
&__meta,
|
||||
&__sidebar-fields {
|
||||
padding-left: var(--gutter-h);
|
||||
[dir=ltr] & {
|
||||
padding-left: var(--gutter-h);
|
||||
}
|
||||
[dir=rtl] & {
|
||||
padding-right: var(--gutter-h);
|
||||
}
|
||||
}
|
||||
|
||||
&__api-url {
|
||||
|
||||
@@ -15,7 +15,7 @@ import { usePreferences } from '../../utilities/Preferences';
|
||||
|
||||
const AccountView: React.FC = () => {
|
||||
const { state: locationState } = useLocation<{ data: unknown }>();
|
||||
const locale = useLocale();
|
||||
const { code: locale } = useLocale();
|
||||
const { setStepNav } = useStepNav();
|
||||
const { user } = useAuth();
|
||||
const userRef = useRef(user);
|
||||
|
||||
@@ -40,10 +40,17 @@
|
||||
position: fixed;
|
||||
width: base(15);
|
||||
height: 100%;
|
||||
top: 0;
|
||||
right: 0;
|
||||
overflow: visible;
|
||||
border-left: 1px solid var(--theme-elevation-100);
|
||||
[dir=ltr] & {
|
||||
top: 0;
|
||||
right: 0;
|
||||
border-left: 1px solid var(--theme-elevation-100);
|
||||
}
|
||||
[dir=rtl] & {
|
||||
top: 0;
|
||||
left: 0;
|
||||
border-right: 1px solid var(--theme-elevation-100);
|
||||
}
|
||||
}
|
||||
|
||||
&__sidebar {
|
||||
@@ -60,12 +67,22 @@
|
||||
|
||||
&__document-actions,
|
||||
&__meta,
|
||||
&__sidebar-fields {
|
||||
padding-left: base(1.5);
|
||||
&__sidebar-fields {
|
||||
[dir=ltr] & {
|
||||
padding-left: base(1.5);
|
||||
}
|
||||
[dir=rtl] & {
|
||||
padding-right: base(1.5);
|
||||
}
|
||||
}
|
||||
|
||||
&__sidebar-fields {
|
||||
padding-right: $baseline;
|
||||
[dir=ltr] & {
|
||||
padding-right: $baseline;
|
||||
}
|
||||
[dir=rtl] & {
|
||||
padding-left: $baseline;
|
||||
}
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $baseline;
|
||||
@@ -88,7 +105,13 @@
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
padding-right: $baseline;
|
||||
[dir=ltr] & {
|
||||
padding-right: $baseline;
|
||||
}
|
||||
[dir=rtl] & {
|
||||
padding-left: $baseline;
|
||||
}
|
||||
|
||||
|
||||
>* {
|
||||
position: relative;
|
||||
@@ -104,11 +127,21 @@
|
||||
}
|
||||
|
||||
>*:first-child {
|
||||
margin-right: base(.5);
|
||||
[dir=ltr] & {
|
||||
margin-right: base(.5);
|
||||
}
|
||||
[dir=rtl] & {
|
||||
margin-left: base(.5);
|
||||
}
|
||||
}
|
||||
|
||||
>*:last-child {
|
||||
margin-left: base(.5);
|
||||
[dir=ltr] & {
|
||||
margin-left: base(.5);
|
||||
}
|
||||
[dir=rtl] & {
|
||||
margin-right: base(.5);
|
||||
}
|
||||
}
|
||||
|
||||
.form-submit {
|
||||
@@ -133,7 +166,12 @@
|
||||
&__meta {
|
||||
margin: auto 0 $baseline;
|
||||
padding-top: $baseline;
|
||||
padding-right: $baseline;
|
||||
[dir=ltr] & {
|
||||
padding-right: $baseline;
|
||||
}
|
||||
[dir=rtl] & {
|
||||
padding-left: $baseline;
|
||||
}
|
||||
list-style: none;
|
||||
|
||||
li {
|
||||
@@ -211,7 +249,12 @@
|
||||
|
||||
&__sidebar-fields {
|
||||
padding-top: 0;
|
||||
padding-right: var(--gutter-h);
|
||||
[dir=ltr] & {
|
||||
padding-right: var(--gutter-h);
|
||||
}
|
||||
[dir=rtl] & {
|
||||
padding-left: var(--gutter-h);
|
||||
}
|
||||
gap: base(0.5);
|
||||
|
||||
.preview-btn {
|
||||
|
||||
@@ -16,7 +16,7 @@ import { usePreferences } from '../../utilities/Preferences';
|
||||
|
||||
const GlobalView: React.FC<IndexProps> = (props) => {
|
||||
const { state: locationState } = useLocation<{ data?: Record<string, unknown> }>();
|
||||
const locale = useLocale();
|
||||
const { code: locale } = useLocale();
|
||||
const { setStepNav } = useStepNav();
|
||||
const { user } = useAuth();
|
||||
const [initialState, setInitialState] = useState<Fields>();
|
||||
|
||||
@@ -4,16 +4,27 @@
|
||||
margin-bottom: base(2);
|
||||
|
||||
&__locale-label {
|
||||
margin-right: base(.25);
|
||||
background: var(--theme-elevation-100);
|
||||
padding: base(.25);
|
||||
border-radius: $style-radius-m;
|
||||
[dir=ltr] & {
|
||||
margin-right: base(.25);
|
||||
}
|
||||
[dir=rtl] & {
|
||||
margin-left: base(.25);
|
||||
}
|
||||
}
|
||||
|
||||
&__wrap {
|
||||
margin: base(.5) 0;
|
||||
padding-left: base(.5);
|
||||
border-left: $style-stroke-width-s solid var(--theme-elevation-150);
|
||||
[dir=ltr] & {
|
||||
padding-left: base(.5);
|
||||
border-left: $style-stroke-width-s solid var(--theme-elevation-150);
|
||||
}
|
||||
[dir=rtl] & {
|
||||
padding-right: base(.5);
|
||||
border-right: $style-stroke-width-s solid var(--theme-elevation-150);
|
||||
}
|
||||
}
|
||||
|
||||
&__no-rows {
|
||||
|
||||
@@ -2,7 +2,13 @@
|
||||
|
||||
.nested-diff {
|
||||
&__wrap--gutter {
|
||||
padding-left: base(1);
|
||||
border-left: $style-stroke-width-s solid var(--theme-elevation-150);
|
||||
[dir=ltr] & {
|
||||
padding-left: base(1);
|
||||
border-left: $style-stroke-width-s solid var(--theme-elevation-150);
|
||||
}
|
||||
[dir=rtl] & {
|
||||
padding-right: base(1);
|
||||
border-right: $style-stroke-width-s solid var(--theme-elevation-150);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,12 @@
|
||||
|
||||
.relationship-diff {
|
||||
&__locale-label {
|
||||
margin-right: base(.25);
|
||||
[dir=ltr] & {
|
||||
margin-right: base(.25);
|
||||
}
|
||||
[dir=rtl] & {
|
||||
margin-left: base(.25);
|
||||
}
|
||||
background: var(--theme-elevation-100);
|
||||
padding: base(.25);
|
||||
border-radius: $style-radius-m;
|
||||
|
||||
@@ -63,7 +63,7 @@ const generateLabelFromValue = (
|
||||
const Relationship: React.FC<Props & { field: RelationshipField }> = ({ field, version, comparison }) => {
|
||||
const { collections } = useConfig();
|
||||
const { t, i18n } = useTranslation('general');
|
||||
const locale = useLocale();
|
||||
const { code: locale } = useLocale();
|
||||
|
||||
let placeholder = '';
|
||||
|
||||
|
||||
@@ -2,7 +2,12 @@
|
||||
|
||||
.select-diff {
|
||||
&__locale-label {
|
||||
margin-right: base(.25);
|
||||
[dir=ltr] & {
|
||||
margin-right: base(.25);
|
||||
}
|
||||
[dir=rtl] & {
|
||||
margin-left: base(.25);
|
||||
}
|
||||
background: var(--theme-elevation-100);
|
||||
padding: base(.25);
|
||||
border-radius: $style-radius-m;
|
||||
|
||||
@@ -2,7 +2,12 @@
|
||||
|
||||
.text-diff {
|
||||
&__locale-label {
|
||||
margin-right: base(.25);
|
||||
[dir=ltr] & {
|
||||
margin-right: base(.25);
|
||||
}
|
||||
[dir=rtl] & {
|
||||
margin-left: base(.25);
|
||||
}
|
||||
background: var(--theme-elevation-100);
|
||||
padding: base(.25);
|
||||
border-radius: $style-radius-m;
|
||||
|
||||
@@ -14,7 +14,12 @@
|
||||
}
|
||||
|
||||
.btn {
|
||||
margin-right: $baseline;
|
||||
[dir=ltr] & {
|
||||
margin-right: $baseline;
|
||||
}
|
||||
[dir=rtl] & {
|
||||
margin-left: $baseline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,8 +19,8 @@ const SelectLocales: React.FC<Props> = ({ onChange, value, options }) => {
|
||||
isMulti
|
||||
placeholder={t('selectLocales')}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
options={options}
|
||||
value={value.map(({ code }) => ({ value: code, label: code }))}
|
||||
options={options.map(({ code }) => ({ value: code, label: code }))}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -32,10 +32,10 @@ const VersionView: React.FC<Props> = ({ collection, global }) => {
|
||||
const { setStepNav } = useStepNav();
|
||||
const { params: { id, versionID } } = useRouteMatch<{ id?: string, versionID: string }>();
|
||||
const [compareValue, setCompareValue] = useState<CompareOption>(mostRecentVersionOption);
|
||||
const [localeOptions] = useState<LocaleOption[]>(() => (localization ? localization.locales.map((locale) => ({ label: locale, value: locale })) : []));
|
||||
const [localeOptions] = useState<LocaleOption[]>(() => (localization ? localization.locales : []));
|
||||
const [locales, setLocales] = useState<LocaleOption[]>(localeOptions);
|
||||
const { permissions } = useAuth();
|
||||
const locale = useLocale();
|
||||
const { code: locale } = useLocale();
|
||||
const { t, i18n } = useTranslation('version');
|
||||
const { docPermissions } = useDocumentInfo();
|
||||
|
||||
@@ -214,7 +214,7 @@ const VersionView: React.FC<Props> = ({ collection, global }) => {
|
||||
|
||||
{doc?.version && (
|
||||
<RenderFieldsToDiff
|
||||
locales={locales ? locales.map(({ value }) => value) : []}
|
||||
locales={locales ? locales.map(({ code }) => code) : []}
|
||||
fields={fields}
|
||||
fieldComponents={fieldComponents}
|
||||
fieldPermissions={fieldPermissions}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { SanitizedGlobalConfig } from '../../../../globals/config/types';
|
||||
|
||||
export type LocaleOption = {
|
||||
label: string
|
||||
value: string
|
||||
code: string
|
||||
}
|
||||
|
||||
export type CompareOption = {
|
||||
|
||||
@@ -23,7 +23,12 @@
|
||||
}
|
||||
|
||||
&__parent-doc-pills {
|
||||
margin-left: auto;
|
||||
[dir=ltr] & {
|
||||
margin-left: auto;
|
||||
}
|
||||
[dir=rtl] & {
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.table {
|
||||
@@ -44,8 +49,14 @@
|
||||
}
|
||||
|
||||
&__page-info {
|
||||
margin-right: base(1);
|
||||
margin-left: auto;
|
||||
[dir=ltr] & {
|
||||
margin-right: base(1);
|
||||
margin-left: auto;
|
||||
}
|
||||
[dir=rtl] & {
|
||||
margin-left: base(1);
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@include mid-break {
|
||||
@@ -58,7 +69,12 @@
|
||||
}
|
||||
|
||||
&__page-info {
|
||||
margin-left: 0;
|
||||
[dir=ltr] & {
|
||||
margin-left: 0;
|
||||
}
|
||||
[dir=rtl] & {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.paginator {
|
||||
|
||||
@@ -7,7 +7,12 @@
|
||||
|
||||
.btn {
|
||||
margin-top: 0;
|
||||
margin-right: base(.5);
|
||||
[dir=ltr] & {
|
||||
margin-right: base(.5);
|
||||
}
|
||||
[dir=rtl] & {
|
||||
margin-left: base(.5);
|
||||
}
|
||||
}
|
||||
|
||||
&__api-key-label {
|
||||
|
||||
@@ -37,19 +37,20 @@
|
||||
&__drop-zone {
|
||||
align-items: center;
|
||||
border: 1px dotted var(--theme-elevation-400);
|
||||
gap: $baseline;
|
||||
|
||||
.btn {
|
||||
margin: 0 $baseline 0 0;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__file-selected {
|
||||
gap: $baseline;
|
||||
flex-direction: column;
|
||||
.btn {
|
||||
@include formInput;
|
||||
position: relative;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
margin: 0;
|
||||
margin-left: -1px;
|
||||
display: flex;
|
||||
padding: 0 base(0.5);
|
||||
|
||||
@@ -30,7 +30,12 @@
|
||||
display: flex;
|
||||
|
||||
li {
|
||||
margin-right: base(.75);
|
||||
[dir=ltr] & {
|
||||
margin-right: base(.75);
|
||||
}
|
||||
[dir=rtl] & {
|
||||
margin-left: base(.75);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,10 +49,17 @@
|
||||
position: fixed;
|
||||
width: base(15);
|
||||
height: 100%;
|
||||
top: 0;
|
||||
right: 0;
|
||||
overflow: visible;
|
||||
border-left: 1px solid var(--theme-elevation-100);
|
||||
[dir=ltr] & {
|
||||
top: 0;
|
||||
right: 0;
|
||||
border-left: 1px solid var(--theme-elevation-100);
|
||||
}
|
||||
[dir=rtl] & {
|
||||
top: 0;
|
||||
left: 0;
|
||||
border-right: 1px solid var(--theme-elevation-100);
|
||||
}
|
||||
}
|
||||
|
||||
&__sidebar {
|
||||
@@ -66,11 +78,21 @@
|
||||
&__document-actions,
|
||||
&__meta,
|
||||
&__sidebar-fields {
|
||||
padding-left: base(1.5);
|
||||
[dir=ltr] & {
|
||||
padding-left: base(1.5);
|
||||
}
|
||||
[dir=rtl] & {
|
||||
padding-right: base(1.5);
|
||||
}
|
||||
}
|
||||
|
||||
&__document-actions {
|
||||
padding-right: $baseline;
|
||||
[dir=ltr] & {
|
||||
padding-right: $baseline;
|
||||
}
|
||||
[dir=rtl] & {
|
||||
padding-left: $baseline;
|
||||
}
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: var(--z-nav);
|
||||
@@ -93,11 +115,21 @@
|
||||
}
|
||||
|
||||
>*:first-child {
|
||||
margin-right: base(.5);
|
||||
[dir=ltr] & {
|
||||
margin-right: base(.5);
|
||||
}
|
||||
[dir=rtl] & {
|
||||
margin-left: base(.5);
|
||||
}
|
||||
}
|
||||
|
||||
>*:last-child {
|
||||
margin-left: base(.5);
|
||||
[dir=ltr] & {
|
||||
margin-left: base(.5);
|
||||
}
|
||||
[dir=rtl] & {
|
||||
margin-right: base(.5);
|
||||
}
|
||||
}
|
||||
|
||||
.form-submit {
|
||||
@@ -120,7 +152,12 @@
|
||||
}
|
||||
|
||||
&__sidebar-fields {
|
||||
padding-right: $baseline;
|
||||
[dir=ltr] & {
|
||||
padding-right: $baseline;
|
||||
}
|
||||
[dir=rtl] & {
|
||||
padding-left: $baseline;
|
||||
}
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $baseline;
|
||||
@@ -141,7 +178,12 @@
|
||||
&__meta {
|
||||
margin: auto 0 $baseline 0;
|
||||
padding-top: $baseline;
|
||||
padding-right: $baseline;
|
||||
[dir=ltr] & {
|
||||
padding-right: $baseline;
|
||||
}
|
||||
[dir=rtl] & {
|
||||
padding-left: $baseline;
|
||||
}
|
||||
list-style: none;
|
||||
|
||||
li {
|
||||
@@ -192,7 +234,12 @@
|
||||
|
||||
&__meta {
|
||||
border-top: 1px solid var(--theme-elevation-100);
|
||||
padding-right: var(--gutter-h);
|
||||
[dir=ltr] & {
|
||||
padding-right: var(--gutter-h);
|
||||
}
|
||||
[dir=rtl] & {
|
||||
padding-left: var(--gutter-h);
|
||||
}
|
||||
}
|
||||
|
||||
&__form {
|
||||
@@ -226,7 +273,12 @@
|
||||
|
||||
&__sidebar-fields {
|
||||
padding-top: 0;
|
||||
padding-right: var(--gutter-h);
|
||||
[dir=ltr] & {
|
||||
padding-right: var(--gutter-h);
|
||||
}
|
||||
[dir=rtl] & {
|
||||
padding-left: var(--gutter-h);
|
||||
}
|
||||
gap: base(0.5);
|
||||
|
||||
.preview-btn {
|
||||
|
||||
@@ -34,7 +34,7 @@ const EditView: React.FC<IndexProps> = (props) => {
|
||||
const [collection] = useState(() => ({ ...incomingCollection, fields }));
|
||||
const [redirect, setRedirect] = useState<string>();
|
||||
|
||||
const locale = useLocale();
|
||||
const { code: locale } = useLocale();
|
||||
const { serverURL, routes: { admin, api } } = useConfig();
|
||||
const { params: { id } = {} } = useRouteMatch<Record<string, string>>();
|
||||
const history = useHistory();
|
||||
|
||||
@@ -10,7 +10,12 @@
|
||||
color: var(--theme-elevation-800);
|
||||
border-radius: $style-radius-s;
|
||||
padding: 0 base(0.25);
|
||||
padding-left: base(0.0875 + 0.25);
|
||||
[dir=ltr] & {
|
||||
padding-left: base(0.0875 + 0.25);
|
||||
}
|
||||
[dir=rtl] & {
|
||||
padding-right: base(0.0875 + 0.25);
|
||||
}
|
||||
|
||||
background: var(--theme-elevation-100);
|
||||
color: var(--theme-elevation-800);
|
||||
|
||||
@@ -10,7 +10,12 @@
|
||||
color: var(--theme-elevation-800);
|
||||
border-radius: $style-radius-s;
|
||||
padding: 0 base(0.25);
|
||||
padding-left: base(0.0875 + 0.25);
|
||||
[dir=ltr] & {
|
||||
padding-left: base(0.0875 + 0.25);
|
||||
}
|
||||
[dir=rtl] & {
|
||||
padding-right: base(0.0875 + 0.25);
|
||||
}
|
||||
|
||||
background: var(--theme-elevation-100);
|
||||
color: var(--theme-elevation-800);
|
||||
|
||||
@@ -13,6 +13,11 @@
|
||||
|
||||
&__filename {
|
||||
align-self: center;
|
||||
margin-left: base(1);
|
||||
[dir=ltr] & {
|
||||
margin-left: base(1);
|
||||
}
|
||||
[dir=rtl] & {
|
||||
margin-right: base(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,12 @@
|
||||
color: var(--theme-elevation-800);
|
||||
border-radius: $style-radius-s;
|
||||
padding: 0 base(0.25);
|
||||
padding-left: base(0.0875 + 0.25);
|
||||
[dir=ltr] & {
|
||||
padding-left: base(0.0875 + 0.25);
|
||||
}
|
||||
[dir=rtl] & {
|
||||
padding-right: base(0.0875 + 0.25);
|
||||
}
|
||||
|
||||
background: var(--theme-elevation-100);
|
||||
color: var(--theme-elevation-800);
|
||||
|
||||
@@ -31,7 +31,7 @@ export const RelationshipProvider: React.FC<{ children?: React.ReactNode }> = ({
|
||||
const debouncedDocuments = useDebounce(documents, 100);
|
||||
const config = useConfig();
|
||||
const { i18n } = useTranslation();
|
||||
const locale = useLocale();
|
||||
const { code: locale } = useLocale();
|
||||
const prevLocale = useRef(locale);
|
||||
|
||||
const {
|
||||
|
||||
@@ -32,7 +32,7 @@ export const SelectionProvider: React.FC<Props> = ({ children, docs = [], totalD
|
||||
const contextRef = useRef({} as SelectionContext);
|
||||
|
||||
const history = useHistory();
|
||||
const locale = useLocale();
|
||||
const { code: locale } = useLocale();
|
||||
const [selected, setSelected] = useState<SelectionContext['selected']>({});
|
||||
const [selectAll, setSelectAll] = useState<SelectAllStatus>(SelectAllStatus.None);
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
@@ -14,9 +14,10 @@
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
flex-wrap: wrap;
|
||||
gap: base(.75);
|
||||
|
||||
h1 {
|
||||
margin: 0 base(.75) 0 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
@@ -57,8 +58,14 @@
|
||||
}
|
||||
|
||||
&__page-info {
|
||||
margin-right: base(1);
|
||||
margin-left: auto;
|
||||
[dir=ltr] & {
|
||||
margin-right: base(1);
|
||||
margin-left: auto;
|
||||
}
|
||||
[dir=rtl] & {
|
||||
margin-left: base(1);
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&__list-selection {
|
||||
|
||||
@@ -33,7 +33,7 @@ const usePayloadAPI: UsePayloadAPI = (url, options = {}) => {
|
||||
const [params, setParams] = useState(initialParams);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isError, setIsError] = useState(false);
|
||||
const locale = useLocale();
|
||||
const { code: locale } = useLocale();
|
||||
|
||||
const search = queryString.stringify({
|
||||
locale,
|
||||
|
||||
@@ -153,6 +153,10 @@ $focus-box-shadow: 0 0 0 $style-stroke-width-m var(--theme-success-500);
|
||||
padding: base(.5) base(.75);
|
||||
-webkit-appearance: none;
|
||||
|
||||
&[data-rtl='true'] {
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
&::-webkit-input-placeholder {
|
||||
color: var(--theme-elevation-400);
|
||||
font-weight: normal;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import merge from 'deepmerge';
|
||||
import { isPlainObject } from 'is-plain-object';
|
||||
import { Config, SanitizedConfig } from './types';
|
||||
import type { Config, LocalizationConfigWithLabels, LocalizationConfigWithNoLabels, SanitizedConfig, SanitizedLocalizationConfig } from './types';
|
||||
import { defaultUserCollection } from '../auth/defaultUser';
|
||||
import sanitizeCollection from '../collections/config/sanitize';
|
||||
import { InvalidConfiguration } from '../errors';
|
||||
@@ -45,6 +45,32 @@ export const sanitizeConfig = (config: Config): SanitizedConfig => {
|
||||
|
||||
sanitizedConfig.admin = sanitizeAdmin(sanitizedConfig as SanitizedConfig);
|
||||
|
||||
if (sanitizedConfig.localization && sanitizedConfig.localization.locales?.length > 0) {
|
||||
// clone localization config so to not break everything
|
||||
const firstLocale = sanitizedConfig.localization.locales[0];
|
||||
if (typeof firstLocale === 'string') {
|
||||
(sanitizedConfig.localization as SanitizedLocalizationConfig).localeCodes = [...(sanitizedConfig.localization as LocalizationConfigWithNoLabels).locales];
|
||||
|
||||
// is string[], so convert to Locale[]
|
||||
(sanitizedConfig.localization as SanitizedLocalizationConfig).locales = (sanitizedConfig.localization as LocalizationConfigWithNoLabels).locales.map((locale) => ({
|
||||
label: locale,
|
||||
code: locale,
|
||||
rtl: false,
|
||||
toString: () => locale,
|
||||
}));
|
||||
} else {
|
||||
// is Locale[], so convert to string[] for localeCodes
|
||||
(sanitizedConfig.localization as SanitizedLocalizationConfig).localeCodes = (sanitizedConfig.localization as SanitizedLocalizationConfig).locales.reduce((locales, locale) => {
|
||||
locales.push(locale.code);
|
||||
return locales;
|
||||
}, [] as string[]);
|
||||
|
||||
(sanitizedConfig.localization as SanitizedLocalizationConfig).locales = (sanitizedConfig.localization as LocalizationConfigWithLabels).locales.map((locale) => ({
|
||||
...locale,
|
||||
toString: () => locale.code,
|
||||
}));
|
||||
}
|
||||
}
|
||||
sanitizedConfig.collections.push(getPreferencesCollection(sanitizedConfig));
|
||||
|
||||
sanitizedConfig.collections.push(migrationsCollection);
|
||||
|
||||
@@ -163,15 +163,25 @@ export default joi.object({
|
||||
disable: joi.boolean(),
|
||||
schemaOutputFile: joi.string(),
|
||||
}),
|
||||
localization: joi.alternatives()
|
||||
.try(
|
||||
joi.object().keys({
|
||||
locales: joi.array().items(joi.string()),
|
||||
defaultLocale: joi.string(),
|
||||
fallback: joi.boolean(),
|
||||
}),
|
||||
joi.boolean(),
|
||||
),
|
||||
localization: joi.alternatives().try(
|
||||
joi.object().keys({
|
||||
locales: joi.alternatives().try(
|
||||
joi.array().items(
|
||||
joi.object().keys({
|
||||
label: joi.string(),
|
||||
code: joi.string(),
|
||||
rtl: joi.boolean(),
|
||||
toString: joi.func(),
|
||||
}),
|
||||
),
|
||||
joi.array().items(joi.string()),
|
||||
),
|
||||
localeCodes: joi.array().items(joi.string()),
|
||||
defaultLocale: joi.string(),
|
||||
fallback: joi.boolean(),
|
||||
}),
|
||||
joi.boolean(),
|
||||
),
|
||||
hooks: joi.object().keys({
|
||||
afterError: joi.func(),
|
||||
}),
|
||||
|
||||
@@ -8,6 +8,7 @@ import GraphQL from 'graphql';
|
||||
import React from 'react';
|
||||
import { DestinationStream, LoggerOptions } from 'pino';
|
||||
import type { InitOptions as i18nInitOptions } from 'i18next';
|
||||
import { Validate } from '../fields/config/types';
|
||||
import { Payload } from '../payload';
|
||||
import { AfterErrorHook, CollectionConfig, SanitizedCollectionConfig } from '../collections/config/types';
|
||||
import { GlobalConfig, SanitizedGlobalConfig } from '../globals/config/types';
|
||||
@@ -17,6 +18,10 @@ import { User } from '../auth/types';
|
||||
import { DatabaseAdapter } from '../database/types';
|
||||
import type { PayloadBundler } from '../bundlers/types';
|
||||
|
||||
type Prettify<T> = {
|
||||
[K in keyof T]: T[K];
|
||||
} & NonNullable<unknown>;
|
||||
|
||||
type Email = {
|
||||
fromName: string;
|
||||
fromAddress: string;
|
||||
@@ -208,24 +213,74 @@ export type AdminRoute = {
|
||||
sensitive?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* @see https://payloadcms.com/docs/configuration/localization#localization
|
||||
*/
|
||||
export type LocalizationConfig = {
|
||||
export type Locale = {
|
||||
/**
|
||||
* List of supported locales
|
||||
* @exanple `["en", "es", "fr", "nl", "de", "jp"]`
|
||||
* label of supported locale
|
||||
* @example "English"
|
||||
*/
|
||||
locales: string[];
|
||||
label: string;
|
||||
/**
|
||||
* value of supported locale
|
||||
* @example "en"
|
||||
*/
|
||||
code: string;
|
||||
/**
|
||||
* if true, defaults textAligmnent on text fields to RTL
|
||||
*/
|
||||
rtl?: boolean;
|
||||
};
|
||||
|
||||
export type BaseLocalizationConfig = {
|
||||
/**
|
||||
* Locale for users that have not expressed their preference for a specific locale
|
||||
* @exanple `"en"`
|
||||
* @example `"en"`
|
||||
*/
|
||||
defaultLocale: string;
|
||||
/** Set to `true` to let missing values in localised fields fall back to the values in `defaultLocale` */
|
||||
fallback?: boolean;
|
||||
};
|
||||
|
||||
export type LocalizationConfigWithNoLabels = Prettify<
|
||||
BaseLocalizationConfig & {
|
||||
/**
|
||||
* List of supported locales
|
||||
* @example `["en", "es", "fr", "nl", "de", "jp"]`
|
||||
*/
|
||||
locales: string[];
|
||||
}
|
||||
>;
|
||||
|
||||
export type LocalizationConfigWithLabels = Prettify<
|
||||
BaseLocalizationConfig & {
|
||||
/**
|
||||
* List of supported locales with labels
|
||||
* @example {
|
||||
* label: 'English',
|
||||
* value: 'en',
|
||||
* rtl: false
|
||||
* }
|
||||
*/
|
||||
locales: Locale[];
|
||||
}
|
||||
>;
|
||||
|
||||
export type SanitizedLocalizationConfig = Prettify<
|
||||
LocalizationConfigWithLabels & {
|
||||
/**
|
||||
* List of supported locales
|
||||
* @example `["en", "es", "fr", "nl", "de", "jp"]`
|
||||
*/
|
||||
localeCodes: string[];
|
||||
}
|
||||
>;
|
||||
|
||||
/**
|
||||
* @see https://payloadcms.com/docs/configuration/localization#localization
|
||||
*/
|
||||
export type LocalizationConfig = Prettify<
|
||||
LocalizationConfigWithNoLabels | LocalizationConfigWithLabels
|
||||
>;
|
||||
|
||||
/**
|
||||
* This is the central configuration
|
||||
*
|
||||
@@ -315,8 +370,8 @@ export type Config = {
|
||||
*/
|
||||
afterDashboard?: React.ComponentType<any>[];
|
||||
/**
|
||||
* Add custom components before the email/password field
|
||||
*/
|
||||
* Add custom components before the email/password field
|
||||
*/
|
||||
beforeLogin?: React.ComponentType<any>[];
|
||||
/**
|
||||
* Add custom components after the email/password field
|
||||
@@ -482,7 +537,7 @@ export type Config = {
|
||||
* window: 15 * 60 * 100, // 1.5 minutes,
|
||||
* max: 500,
|
||||
* }
|
||||
*/
|
||||
*/
|
||||
rateLimit?: {
|
||||
window?: number;
|
||||
max?: number;
|
||||
@@ -554,16 +609,17 @@ export type Config = {
|
||||
|
||||
export type SanitizedConfig = Omit<
|
||||
DeepRequired<Config>,
|
||||
'collections' | 'globals' | 'endpoint'
|
||||
'collections' | 'globals' | 'endpoint' | 'localization'
|
||||
> & {
|
||||
collections: SanitizedCollectionConfig[];
|
||||
globals: SanitizedGlobalConfig[];
|
||||
endpoints: Endpoint[];
|
||||
paths: {
|
||||
configDir: string
|
||||
config: string
|
||||
rawConfig: string
|
||||
configDir: string;
|
||||
config: string;
|
||||
rawConfig: string;
|
||||
};
|
||||
localization: false | SanitizedLocalizationConfig;
|
||||
};
|
||||
|
||||
export type EntityDescription =
|
||||
|
||||
@@ -64,7 +64,7 @@ export async function getLocalizedPaths({
|
||||
}
|
||||
|
||||
const nextSegment = pathSegments[i + 1];
|
||||
const nextSegmentIsLocale = localizationConfig && localizationConfig.locales.includes(nextSegment);
|
||||
const nextSegmentIsLocale = localizationConfig && localizationConfig.localeCodes.includes(nextSegment);
|
||||
|
||||
if (nextSegmentIsLocale) {
|
||||
// Skip the next iteration, because it's a locale
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Handler } from 'express';
|
||||
import { defaultOptions } from '../../translations/defaultOptions';
|
||||
|
||||
const i18nMiddleware = (options: InitOptions): Handler => {
|
||||
i18next.use(i18nHTTPMiddleware.LanguageDetector)
|
||||
i18next.use(new i18nHTTPMiddleware.LanguageDetector(defaultOptions.detection))
|
||||
.init({
|
||||
preload: defaultOptions.supportedLngs,
|
||||
...deepmerge(defaultOptions, options || {}),
|
||||
|
||||
@@ -80,6 +80,7 @@ export const text = baseField.keys({
|
||||
joi.string(),
|
||||
),
|
||||
autoComplete: joi.string(),
|
||||
rtl: joi.boolean(),
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -116,6 +117,7 @@ export const textarea = baseField.keys({
|
||||
admin: baseAdminFields.keys({
|
||||
placeholder: joi.string(),
|
||||
rows: joi.number(),
|
||||
rtl: joi.boolean(),
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -446,6 +448,7 @@ export const richText = baseField.keys({
|
||||
joi.func(),
|
||||
),
|
||||
}),
|
||||
rtl: joi.boolean(),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -165,6 +165,7 @@ export type TextField = FieldBase & {
|
||||
admin?: Admin & {
|
||||
placeholder?: Record<string, string> | string
|
||||
autoComplete?: string
|
||||
rtl?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,6 +184,7 @@ export type TextareaField = FieldBase & {
|
||||
admin?: Admin & {
|
||||
placeholder?: Record<string, string> | string
|
||||
rows?: number
|
||||
rtl?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
@@ -411,6 +413,7 @@ export type RichTextField = FieldBase & {
|
||||
link?: {
|
||||
fields?: Field[] | ((args: { defaultFields: Field[], config: SanitizedConfig, i18n: Ii18n }) => Field[]);
|
||||
}
|
||||
rtl?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -150,7 +150,8 @@ export const promise = async ({
|
||||
if (field.localized) {
|
||||
mergeLocaleActions.push(() => {
|
||||
if (req.payload.config.localization) {
|
||||
const localeData = req.payload.config.localization.locales.reduce((localizedValues, locale) => {
|
||||
const { localization } = req.payload.config;
|
||||
const localeData = localization.localeCodes.reduce((localizedValues, locale) => {
|
||||
const fieldValue = locale === req.locale
|
||||
? siblingData[field.name]
|
||||
: siblingDocWithLocales?.[field.name]?.[locale];
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
import { GraphQLEnumType } from 'graphql';
|
||||
import { LocalizationConfig } from '../../config/types';
|
||||
import { SanitizedLocalizationConfig } from '../../config/types';
|
||||
import formatName from '../utilities/formatName';
|
||||
|
||||
const buildFallbackLocaleInputType = (localization: LocalizationConfig): GraphQLEnumType => new GraphQLEnumType({
|
||||
name: 'FallbackLocaleInputType',
|
||||
values: [...localization.locales, 'none'].reduce((values, locale) => ({
|
||||
...values,
|
||||
[formatName(locale)]: {
|
||||
value: locale,
|
||||
},
|
||||
}), {}),
|
||||
});
|
||||
const buildFallbackLocaleInputType = (
|
||||
localization: SanitizedLocalizationConfig,
|
||||
): GraphQLEnumType => {
|
||||
return new GraphQLEnumType({
|
||||
name: 'FallbackLocaleInputType',
|
||||
values: [...localization.localeCodes, 'none'].reduce(
|
||||
(values, locale) => ({
|
||||
...values,
|
||||
[formatName(locale)]: {
|
||||
value: locale,
|
||||
},
|
||||
}),
|
||||
{},
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
export default buildFallbackLocaleInputType;
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
import { GraphQLEnumType, GraphQLScalarType } from 'graphql';
|
||||
import { LocalizationConfig } from '../../config/types';
|
||||
import type { SanitizedLocalizationConfig } from '../../config/types';
|
||||
import formatName from '../utilities/formatName';
|
||||
|
||||
const buildLocaleInputType = (localization: LocalizationConfig): GraphQLEnumType | GraphQLScalarType => {
|
||||
const buildLocaleInputType = (
|
||||
localization: SanitizedLocalizationConfig,
|
||||
): GraphQLEnumType | GraphQLScalarType => {
|
||||
return new GraphQLEnumType({
|
||||
name: 'LocaleInputType',
|
||||
values: localization.locales.reduce((values, locale) => ({
|
||||
...values,
|
||||
[formatName(locale)]: {
|
||||
value: locale,
|
||||
},
|
||||
}), {}),
|
||||
values: localization.localeCodes.reduce(
|
||||
(values, locale) => ({
|
||||
...values,
|
||||
[formatName(locale)]: {
|
||||
value: locale,
|
||||
},
|
||||
}),
|
||||
{},
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,31 +1,37 @@
|
||||
import type { SanitizedLocalizationConfig } from '../config/types';
|
||||
|
||||
/**
|
||||
* sets request locale
|
||||
*
|
||||
* @param localization
|
||||
* @returns {Function}
|
||||
*/
|
||||
export default function localizationMiddleware(localization) {
|
||||
export default function localizationMiddleware(localization: false | SanitizedLocalizationConfig) {
|
||||
const middleware = (req, res, next) => {
|
||||
if (localization) {
|
||||
const validLocales = [...localization.locales, 'all'];
|
||||
const validFallbackLocales = [...localization.locales, 'null'];
|
||||
const validLocales = [...localization.localeCodes, 'all'];
|
||||
const validFallbackLocales = [...localization.localeCodes, 'null'];
|
||||
|
||||
let requestedLocale = req.query.locale || localization.defaultLocale;
|
||||
let requestedFallbackLocale = req.query['fallback-locale'] || localization.defaultLocale;
|
||||
|
||||
if (req.body) {
|
||||
if (req.body.locale) requestedLocale = req.body.locale;
|
||||
if (req.body['fallback-locale']) requestedFallbackLocale = req.body['fallback-locale'];
|
||||
if (req.body['fallback-locale']) { requestedFallbackLocale = req.body['fallback-locale']; }
|
||||
}
|
||||
|
||||
if (requestedFallbackLocale === 'none') requestedFallbackLocale = 'null';
|
||||
if (requestedLocale === '*' || requestedLocale === 'all') requestedLocale = 'all';
|
||||
if (requestedLocale === '*' || requestedLocale === 'all') { requestedLocale = 'all'; }
|
||||
|
||||
if (validLocales.find((locale) => locale === requestedLocale)) {
|
||||
req.locale = requestedLocale;
|
||||
}
|
||||
|
||||
if (validFallbackLocales.find((locale) => locale === requestedFallbackLocale)) {
|
||||
if (
|
||||
validFallbackLocales.find(
|
||||
(locale) => locale === requestedFallbackLocale,
|
||||
)
|
||||
) {
|
||||
req.fallbackLocale = requestedFallbackLocale;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,58 +3,58 @@
|
||||
"authentication": {
|
||||
"account": "الحساب",
|
||||
"accountOfCurrentUser": "حساب المستخدم الحالي",
|
||||
"alreadyActivated": "تم التفعيل بالفعل",
|
||||
"alreadyLoggedIn": "تم تسجيل الدخول بالفعل",
|
||||
"alreadyActivated": "تمّ التّفعيل بالفعل",
|
||||
"alreadyLoggedIn": "تمّ تسجيل الدّخول بالفعل",
|
||||
"apiKey": "مفتاح API",
|
||||
"backToLogin": "العودة إلى تسجيل الدخول",
|
||||
"beginCreateFirstUser": "للبدء، قم بإنشاء مستخدمك الأول.",
|
||||
"backToLogin": "العودة لتسجيل الدخول",
|
||||
"beginCreateFirstUser": "للبدء, قم بإنشاء المستخدم الأوّل.",
|
||||
"changePassword": "تغيير كلمة المرور",
|
||||
"checkYourEmailForPasswordReset": "يرجى التحقق من بريدك الإلكتروني للحصول على رابط يتيح لك إعادة تعيين كلمة المرور بأمان.",
|
||||
"confirmGeneration": "تأكيد التوليد",
|
||||
"checkYourEmailForPasswordReset": "تحقّق من بريدك الإلكتروني بحثًا عن رابط يسمح لك بإعادة تعيين كلمة المرور الخاصّة بك بشكل آمن.",
|
||||
"confirmGeneration": "تأكيد التّوليد",
|
||||
"confirmPassword": "تأكيد كلمة المرور",
|
||||
"createFirstUser": "إنشاء المستخدم الأول",
|
||||
"emailNotValid": "البريد الإلكتروني الذي تم تقديمه غير صالح",
|
||||
"emailSent": "تم إرسال البريد الإلكتروني",
|
||||
"enableAPIKey": "تمكين مفتاح API",
|
||||
"failedToUnlock": "فشل في الفتح",
|
||||
"forceUnlock": "فتح بالقوة",
|
||||
"createFirstUser": "إنشاء المستخدم الأوّل",
|
||||
"emailNotValid": "البريد الإلكتروني غير صالح",
|
||||
"emailSent": "تمّ ارسال البريد الإلكتروني",
|
||||
"enableAPIKey": "تفعيل مفتاح API",
|
||||
"failedToUnlock": "فشل فتح القفل",
|
||||
"forceUnlock": "إجبار فتح القفل",
|
||||
"forgotPassword": "نسيت كلمة المرور",
|
||||
"forgotPasswordEmailInstructions": "يرجى إدخال بريدك الإلكتروني أدناه. ستتلقى رسالة بريد إلكتروني تحتوي على تعليمات حول كيفية إعادة تعيين كلمة المرور الخاصة بك.",
|
||||
"forgotPasswordEmailInstructions": "يرجى إدخال البريد الالكتروني أدناه. ستتلقّى رسالة بريد إلكتروني تحتوي على إرشادات حول كيفيّة إعادة تعيين كلمة المرور الخاصّة بك.",
|
||||
"forgotPasswordQuestion": "هل نسيت كلمة المرور؟",
|
||||
"generate": "توليد",
|
||||
"generateNewAPIKey": "توليد مفتاح API جديد",
|
||||
"generatingNewAPIKeyWillInvalidate": "سيؤدي إنشاء مفتاح API جديد إلى <1>إلغاء صلاحية</1> المفتاح السابق. هل تريد المتابعة؟",
|
||||
"lockUntil": "الإقفال حتى",
|
||||
"logBackIn": "تسجيل الدخول مرة أخرى",
|
||||
"generatingNewAPIKeyWillInvalidate": "سيؤدّي إنشاء مفتاح API جديد إلى <1> إبطال </ 1> المفتاح السّابق. هل أنت متأكّد أنّك تريد المتابعة؟",
|
||||
"lockUntil": "قفل حتى",
|
||||
"logBackIn": "تسجيل الدّخول من جديد",
|
||||
"logOut": "تسجيل الخروج",
|
||||
"loggedIn": "لتسجيل الدخول باستخدام مستخدم آخر، يجب عليك <0>تسجيل الخروج</0> أولاً.",
|
||||
"loggedInChangePassword": "لتغيير كلمة المرور الخاصة بك ، انتقل إلى <0>حسابك</0> وقم بتحرير كلمة المرور هناك.",
|
||||
"loggedOutInactivity": "لقد تم تسجيل خروجك بسبب عدم النشاط.",
|
||||
"loggedOutSuccessfully": "لقد تم تسجيل الخروج بنجاح.",
|
||||
"loggedIn": "لتسجيل الدّخول مع مستخدم آخر ، يجب عليك <0> تسجيل الخروج </0> أوّلاً.",
|
||||
"loggedInChangePassword": "لتغيير كلمة المرور الخاصّة بك ، انتقل إلى <0>حسابك</0> وقم بتعديل كلمة المرور هناك.",
|
||||
"loggedOutInactivity": "لقد تمّ تسجيل الخروج بسبب عدم النّشاط.",
|
||||
"loggedOutSuccessfully": "لقد تمّ تسجيل خروجك بنجاح.",
|
||||
"login": "تسجيل الدخول",
|
||||
"loginAttempts": "محاولات تسجيل الدخول",
|
||||
"loginUser": "مستخدم تسجيل الدخول",
|
||||
"loginWithAnotherUser": "لتسجيل الدخول باستخدام مستخدم آخر ، يجب عليك <0>تسجيل الخروج</0> أولاً.",
|
||||
"loginUser": "تسجيل دخول المستخدم",
|
||||
"loginWithAnotherUser": "لتسجيل الدخول مع مستخدم آخر ، يجب عليك <0> تسجيل الخروج </0> أوّلاً.",
|
||||
"logout": "تسجيل الخروج",
|
||||
"logoutUser": "تسجيل خروج المستخدم",
|
||||
"newAPIKeyGenerated": "تم إنشاء مفتاح API جديد.",
|
||||
"newAccountCreated": "تم إنشاء حساب جديد لتتمكن من الوصول إلى <a href=\"{{serverURL}}\"> {{serverURL}} </a> الرجاء النقر فوق الارتباط التالي أو لصق عنوان URL أدناه في متصفحك للتحقق من بريدك الإلكتروني: <a href=\"{{verificationURL}}\"> {{verificationURL}} </a> <br> بعد التحقق من بريدك الإلكتروني ، ستتمكن من تسجيل الدخول بنجاح. ",
|
||||
"newPassword": "كلمة السر الجديدة",
|
||||
"newAPIKeyGenerated": "تمّ توليد مفتاح API جديد.",
|
||||
"newAccountCreated": "تمّ إنشاء حساب جديد لتتمكّن من الوصول إلى <a href=\"{{serverURL}}\"> {{serverURL}} </a> الرّجاء النّقر فوق الرّابط التّالي أو لصق عنوان URL أدناه في متصفّحّك لتأكيد بريدك الإلكتروني : <a href=\"{{verificationURL}}\"> {{verificationURL}} </a> <br> بعد التّحقّق من بريدك الإلكتروني ، ستتمكّن من تسجيل الدّخول بنجاح.",
|
||||
"newPassword": "كلمة مرور جديدة",
|
||||
"resetPassword": "إعادة تعيين كلمة المرور",
|
||||
"resetPasswordExpiration": "انتهاء صلاحية إعادة تعيين كلمة المرور",
|
||||
"resetPasswordExpiration": "انتهاء صلاحيّة إعادة تعيين كلمة المرور",
|
||||
"resetPasswordToken": "رمز إعادة تعيين كلمة المرور",
|
||||
"resetYourPassword": "إعادة تعيين كلمة المرور الخاصة بك",
|
||||
"stayLoggedIn": "ابق متصلا",
|
||||
"successfullyUnlocked": "تم فتح القفل بنجاح",
|
||||
"unableToVerify": "غير قادر على التحقق",
|
||||
"verified": "تم التحقق",
|
||||
"verifiedSuccessfully": "تم التحقق بنجاح",
|
||||
"verify": "التحقق",
|
||||
"verifyUser": "التحقق من المستخدم",
|
||||
"verifyYourEmail": "تحقق من بريدك الإلكتروني",
|
||||
"youAreInactive": "لم تكن نشطًا لبعض الوقت وسيتم تسجيل خروجك تلقائيًا قريبًا لأمانك الشخصي. هل تريد البقاء متصلًا؟",
|
||||
"youAreReceivingResetPassword": "تتلقى هذا لأنك (أو شخص آخر) قد طلب إعادة تعيين كلمة المرور لحسابك. يرجى النقر على الرابط التالي، أو نسخه ولصقه في المتصفح الخاص بك لإكمال العملية:",
|
||||
"youDidNotRequestPassword": "إذا لم تطلب هذا، يرجى تجاهل هذا البريد الإلكتروني وسيظل كلمة المرور الخاصة بك كما هي."
|
||||
"resetYourPassword": "إعادة تعيين كلمة المرور الخاصّة بك",
|
||||
"stayLoggedIn": "ابق متّصلًا",
|
||||
"successfullyUnlocked": "تمّ فتح القفل بنجاح",
|
||||
"unableToVerify": "غير قادر على التحقق من",
|
||||
"verified": "تمّ التحقّق",
|
||||
"verifiedSuccessfully": "تمّ التحقّق بنجاح",
|
||||
"verify": "قم بالتّحقّق",
|
||||
"verifyUser": "قم بالتّحقّق من المستخدم",
|
||||
"verifyYourEmail": "قم بتأكيد بريدك الألكتروني",
|
||||
"youAreInactive": "لم تكن نشطًا منذ فترة قصيرة وسيتمّ تسجيل خروجك قريبًا تلقائيًا من أجل أمنك. هل ترغب في البقاء مسجّلا؟",
|
||||
"youAreReceivingResetPassword": "أنت تتلقّى هذا البريد الالكتروني لأنّك (أو لأنّ شخص آخر) طلبت إعادة تعيين كلمة المرور لحسابك. الرّجاء النّقر فوق الرّابط التّالي ، أو لصق هذا الرّابط في متصفّحك لإكمال العمليّة:",
|
||||
"youDidNotRequestPassword": "إن لم تطلب هذا ، يرجى تجاهل هذا البريد الإلكتروني وستبقى كلمة مرورك ذاتها بدون تغيير."
|
||||
},
|
||||
"error": {
|
||||
"accountAlreadyActivated": "تم تفعيل هذا الحساب بالفعل.",
|
||||
@@ -70,111 +70,111 @@
|
||||
"invalidFileTypeValue": "نوع ملف غير صالح: {{value}}",
|
||||
"loadingDocument": "حدثت مشكلة أثناء تحميل المستند برقم التعريف {{id}}.",
|
||||
"missingEmail": "البريد الإلكتروني مفقود.",
|
||||
"missingIDOfDocument": "معرف المستند الذي يجب تحديثه مفقود.",
|
||||
"missingIDOfVersion": "معرف الإصدار مفقود.",
|
||||
"missingRequiredData": "البيانات المطلوبة مفقودة.",
|
||||
"noFilesUploaded": "لم يتم تحميل أي ملفات.",
|
||||
"noMatchedField": "لم يتم العثور على حقل مطابق ل\"{{label}}\"",
|
||||
"missingIDOfDocument": "معرّف المستند المراد تحديثه مفقود.",
|
||||
"missingIDOfVersion": "معرّف النسخة مفقود.",
|
||||
"missingRequiredData": "توجد بيانات مطلوبة مفقودة.",
|
||||
"noFilesUploaded": "لم يتمّ رفع أيّة ملفّات.",
|
||||
"noMatchedField": "لم يتمّ العثور على حقل مطابق لـ \"{{label}}\"",
|
||||
"noUser": "لا يوجد مستخدم",
|
||||
"notAllowedToAccessPage": "لا يسمح لك بالوصول إلى هذه الصفحة.",
|
||||
"notAllowedToPerformAction": "لا يسمح لك بأداء هذا الإجراء.",
|
||||
"notFound": "لم يتم العثور على المورد المطلوب.",
|
||||
"previewing": "حدثت مشكلة أثناء معاينة هذا الوثيقة.",
|
||||
"problemUploadingFile": "حدثت مشكلة أثناء تحميل الملف.",
|
||||
"tokenInvalidOrExpired": "الرمز غير صالح أو انتهت صلاحيته.",
|
||||
"unPublishingDocument": "حدثت مشكلة أثناء إلغاء نشر هذه الوثيقة.",
|
||||
"unableToDeleteCount": "غير قادر على حذف {{count}} من بين {{total}} {{label}}.",
|
||||
"unableToUpdateCount": "غير قادر على تحديث {{count}} من بين {{total}} {{label}}.",
|
||||
"unauthorized": "غير مصرح لك، يجب تسجيل الدخول لإجراء هذا الطلب.",
|
||||
"notAllowedToAccessPage": "لا يسمح لك الوصول إلى هذه الصّفحة.",
|
||||
"notAllowedToPerformAction": "لا يسمح لك القيام بهذه العمليّة.",
|
||||
"notFound": "لم يتمّ العثور على المورد المطلوب.",
|
||||
"previewing": "حدث خطأ في اثناء معاينة هذا المستند.",
|
||||
"problemUploadingFile": "حدث خطأ اثناء رفع الملفّ.",
|
||||
"tokenInvalidOrExpired": "الرّمز إمّا غير صالح أو منتهي الصّلاحيّة.",
|
||||
"unPublishingDocument": "حدث خطأ أثناء إلغاء نشر هذا المستند.",
|
||||
"unableToDeleteCount": "يتعذّر حذف {{count}} من {{total}} {{label}}.",
|
||||
"unableToUpdateCount": "يتعذّر تحديث {{count}} من {{total}} {{label}}.",
|
||||
"unauthorized": "غير مصرّح لك ، عليك أن تقوم بتسجيل الدّخول لتتمكّن من تقديم هذا الطّلب.",
|
||||
"unknown": "حدث خطأ غير معروف.",
|
||||
"unspecific": "حدث خطأ.",
|
||||
"userLocked": "تم قفل هذا المستخدم بسبب العديد من محاولات تسجيل الدخول الفاشلة.",
|
||||
"valueMustBeUnique": "يجب أن يكون القيمة فريدة",
|
||||
"verificationTokenInvalid": "رمز التحقق غير صالح."
|
||||
"userLocked": "تمّ قفل هذا المستخدم نظرًا لوجود عدد كبير من محاولات تسجيل الدّخول الغير ناجحة.",
|
||||
"valueMustBeUnique": "على القيمة أن تكون فريدة",
|
||||
"verificationTokenInvalid": "رمز التحقّق غير صالح."
|
||||
},
|
||||
"fields": {
|
||||
"addLabel": "إضافة {{label}}",
|
||||
"addLink": "إضافة رابط",
|
||||
"addNew": "إضافة جديد",
|
||||
"addNewLabel": "إضافة {{label}} جديدة",
|
||||
"addRelationship": "إضافة علاقة",
|
||||
"addUpload": "إضافة تحميل",
|
||||
"block": "منع",
|
||||
"blockType": "نوع المنع",
|
||||
"blocks": "المنعات",
|
||||
"chooseBetweenCustomTextOrDocument": "اختر بين إدخال عنوان URL مخصص أو الربط بمستند آخر.",
|
||||
"chooseDocumentToLink": "اختر المستند الذي تريد الربط به",
|
||||
"chooseFromExisting": "اختر من القائمة الموجودة",
|
||||
"addLabel": "أضف {{label}}",
|
||||
"addLink": "أضف رابط",
|
||||
"addNew": "أضف جديد",
|
||||
"addNewLabel": "أضف {{label}} جديد",
|
||||
"addRelationship": "أضف علاقة",
|
||||
"addUpload": "أضف تحميل",
|
||||
"block": "وحدة محتوى",
|
||||
"blockType": "نوع وحدة المحتوى",
|
||||
"blocks": "وحدات المحتوى",
|
||||
"chooseBetweenCustomTextOrDocument": "اختر بين إدخال عنوان URL نصّي مخصّص أو الرّبط بمستند آخر.",
|
||||
"chooseDocumentToLink": "اختر مستندًا للربط",
|
||||
"chooseFromExisting": "اختر من القائمة",
|
||||
"chooseLabel": "اختر {{label}}",
|
||||
"collapseAll": "طي الكل",
|
||||
"customURL": "عنوان URL مخصص",
|
||||
"editLabelData": "تعديل بيانات {{label}}",
|
||||
"editLink": "تعديل الرابط",
|
||||
"editRelationship": "تعديل العلاقة",
|
||||
"enterURL": "أدخل عنوان URL",
|
||||
"collapseAll": "طيّ الكلّ",
|
||||
"customURL": "URL مخصّص",
|
||||
"editLabelData": "عدّل بيانات {{label}}",
|
||||
"editLink": "عدّل الرّابط",
|
||||
"editRelationship": "عدّل العلاقة",
|
||||
"enterURL": "ادخل عنوان URL",
|
||||
"internalLink": "رابط داخلي",
|
||||
"itemsAndMore": "{{items}} و {{count}} آخرين",
|
||||
"itemsAndMore": "{{items}} و {{count}} أخرى",
|
||||
"labelRelationship": "{{label}} علاقة",
|
||||
"latitude": "خط العرض",
|
||||
"linkType": "نوع الرابط",
|
||||
"linkedTo": "مرتبط بـ <0>{{label}}</0>",
|
||||
"longitude": "خط الطول",
|
||||
"latitude": "خطّ العرض",
|
||||
"linkType": "نوع الرّابط",
|
||||
"linkedTo": "تمّ الرّبط ل <0>{{label}}</0>",
|
||||
"longitude": "خطّ الطّول",
|
||||
"newLabel": "{{label}} جديد",
|
||||
"openInNewTab": "فتح في علامة تبويب جديدة",
|
||||
"passwordsDoNotMatch": "كلمتا المرور غير متطابقتين.",
|
||||
"relatedDocument": "مستند مرتبط",
|
||||
"relationTo": "العلاقة بـ",
|
||||
"removeRelationship": "إزالة العلاقة",
|
||||
"removeUpload": "حذف التحميل",
|
||||
"saveChanges": "حفظ التغييرات",
|
||||
"searchForBlock": "البحث عن كتلة",
|
||||
"selectExistingLabel": "حدد {{label}} الحالي",
|
||||
"selectFieldsToEdit": "تحديد الحقول المراد تحريرها",
|
||||
"showAll": "عرض الكل",
|
||||
"openInNewTab": "الفتح في علامة تبويب جديدة",
|
||||
"passwordsDoNotMatch": "كلمة المرور غير مطابقة.",
|
||||
"relatedDocument": "مستند مربوط",
|
||||
"relationTo": "ربط ل",
|
||||
"removeRelationship": "حذف العلاقة",
|
||||
"removeUpload": "حذف المحتوى المرفوع",
|
||||
"saveChanges": "حفظ التّغييرات",
|
||||
"searchForBlock": "ابحث عن وحدة محتوى",
|
||||
"selectExistingLabel": "اختيار {{label}} من القائمة",
|
||||
"selectFieldsToEdit": "حدّد الحقول اللتي تريد تعديلها",
|
||||
"showAll": "إظهار الكلّ",
|
||||
"swapRelationship": "تبديل العلاقة",
|
||||
"swapUpload": "تبديل التحميل",
|
||||
"textToDisplay": "النص المعروض",
|
||||
"toggleBlock": "تبديل الكتلة",
|
||||
"uploadNewLabel": "{{label}} جديدة للتحميل"
|
||||
"swapUpload": "تبديل المحتوى المرفوع",
|
||||
"textToDisplay": "النصّ الذي تريد إظهاره",
|
||||
"toggleBlock": "Toggle block",
|
||||
"uploadNewLabel": "رفع {{label}} جديد"
|
||||
},
|
||||
"general": {
|
||||
"aboutToDelete": "أنت على وشك حذف {{label}} <1>{{title}}</1>>. هل أنت متأكد؟",
|
||||
"aboutToDelete": "أنت على وشك حذف {{label}} <1>{{title}}</1>. هل أنت متأكّد؟",
|
||||
"aboutToDeleteCount_many": "أنت على وشك حذف {{count}} {{label}}",
|
||||
"aboutToDeleteCount_one": "أنت على وشك حذف {{count}} {{label}}",
|
||||
"aboutToDeleteCount_other": "أنت على وشك حذف {{count}} {{label}}",
|
||||
"addBelow": "إضافة أسفل",
|
||||
"addFilter": "إضافة تصفية",
|
||||
"adminTheme": "سمة المسؤول",
|
||||
"addBelow": "أضف في الاسفل",
|
||||
"addFilter": "أضف فلتر",
|
||||
"adminTheme": "شكل واجهة المستخدم",
|
||||
"and": "و",
|
||||
"ascending": "تصاعدي",
|
||||
"automatic": "تلقائي",
|
||||
"backToDashboard": "العودة إلى لوحة التحكم",
|
||||
"backToDashboard": "العودة للوحة التّحكّم",
|
||||
"cancel": "إلغاء",
|
||||
"changesNotSaved": "لم يتم حفظ تغييراتك. إذا غادرت الآن ، فستفقد التغييرات الخاصة بك.",
|
||||
"changesNotSaved": "لم يتمّ حفظ التّغييرات. إن غادرت الآن ، ستفقد تغييراتك.",
|
||||
"close": "إغلاق",
|
||||
"collections": "المجموعات",
|
||||
"columnToSort": "العمود المراد الفرز عليه",
|
||||
"columnToSort": "التّرتيب حسب العامود",
|
||||
"columns": "الأعمدة",
|
||||
"confirm": "تأكيد",
|
||||
"confirmDeletion": "تأكيد الحذف",
|
||||
"confirmDuplication": "تأكيد الاستنساخ",
|
||||
"copied": "تم النسخ",
|
||||
"confirmDuplication": "تأكيد التّكرار",
|
||||
"copied": "تمّ النّسخ",
|
||||
"copy": "نسخ",
|
||||
"create": "إنشاء",
|
||||
"createNew": "إنشاء جديد",
|
||||
"createNew": "أنشاء جديد",
|
||||
"createNewLabel": "إنشاء {{label}} جديد",
|
||||
"created": "تم الإنشاء",
|
||||
"createdAt": "تاريخ الإنشاء",
|
||||
"creating": "جاري الإنشاء",
|
||||
"dark": "داكن",
|
||||
"dashboard": "لوحة التحكم",
|
||||
"created": "تمّ الإنشاء",
|
||||
"createdAt": "تمّ الإنشاء في",
|
||||
"creating": "يتمّ الإنشاء",
|
||||
"dark": "غامق",
|
||||
"dashboard": "لوحة التّحكّم",
|
||||
"delete": "حذف",
|
||||
"deletedCountSuccessfully": "تم حذف {{count}} {{label}} بنجاح.",
|
||||
"deletedSuccessfully": "تم الحذف بنجاح.",
|
||||
"deleting": "جاري الحذف...",
|
||||
"deletedCountSuccessfully": "تمّ حذف {{count}} {{label}} بنجاح.",
|
||||
"deletedSuccessfully": "تمّ الحذف بنجاح.",
|
||||
"deleting": "يتمّ الحذف...",
|
||||
"descending": "تنازلي",
|
||||
"duplicate": "استنساخ",
|
||||
"duplicateWithoutSaving": "استنساخ بدون حفظ التغييرات",
|
||||
"duplicate": "تكرار",
|
||||
"duplicateWithoutSaving": "تكرار بدون حفظ التّغييرات",
|
||||
"edit": "تعديل",
|
||||
"editLabel": "تعديل {{label}}",
|
||||
"editing": "جاري التعديل",
|
||||
@@ -186,45 +186,45 @@
|
||||
"email": "البريد الإلكتروني",
|
||||
"emailAddress": "عنوان البريد الإلكتروني",
|
||||
"enterAValue": "أدخل قيمة",
|
||||
"fallbackToDefaultLocale": "الرجوع إلى اللغة الافتراضية",
|
||||
"filter": "تصفية",
|
||||
"filterWhere": "تصفية {{label}} حيث",
|
||||
"filters": "عوامل التصفية",
|
||||
"globals": "عامة",
|
||||
"language": "اللغة",
|
||||
"lastModified": "آخر تعديل",
|
||||
"leaveAnyway": "المغادرة على أي حال",
|
||||
"fallbackToDefaultLocale": "يتمّ استخدام اللّغة الافتراضيّة",
|
||||
"filter": "فلتر",
|
||||
"filterWhere": "فلتر {{label}} أينما",
|
||||
"filters": "فلاتر",
|
||||
"globals": "المجموعات العامّة",
|
||||
"language": "اللّغة",
|
||||
"lastModified": "آخر تعديل في",
|
||||
"leaveAnyway": "المغادرة على أيّة حال",
|
||||
"leaveWithoutSaving": "المغادرة بدون حفظ",
|
||||
"light": "فاتح",
|
||||
"loading": "جاري التحميل",
|
||||
"locales": "اللغات",
|
||||
"moveDown": "تحريك للأسفل",
|
||||
"moveUp": "تحريك للأعلى",
|
||||
"loading": "يتمّ التّحميل",
|
||||
"locales": "اللّغات",
|
||||
"moveDown": "التّحريك إلى الأسفل",
|
||||
"moveUp": "التّحريك إلى الأعلى",
|
||||
"newPassword": "كلمة مرور جديدة",
|
||||
"noFiltersSet": "لم يتم تعيين أي عوامل تصفية",
|
||||
"noLabel": "<لا {{label}}>",
|
||||
"noResults": "لا يوجد {{label}}. إما أن لا {{label}} موجودة حتى الآن أو لا تتطابق مع عوامل التصفية التي حددتها أعلاه.",
|
||||
"noValue": "لا يوجد قيمة",
|
||||
"none": "لا شيء",
|
||||
"notFound": "غير موجود",
|
||||
"nothingFound": "لم يتم العثور على شيء",
|
||||
"noFiltersSet": "لم يتمّ تحديد فلتر",
|
||||
"noLabel": "<لا يوجد {{label}}>",
|
||||
"noResults": "لم يتمّ العثور على {{label}}. إمّا أنّه لا يوجد {{label}} حتّى الآن أو أنّه لا يتطابق أيّ منها مع الفلاتر التّي حدّدتها أعلاه.",
|
||||
"noValue": "لا توجد قيمة",
|
||||
"none": "None",
|
||||
"notFound": "غير معثور عليه",
|
||||
"nothingFound": "لم يتمّ العثور على شيء",
|
||||
"of": "من",
|
||||
"or": "أو",
|
||||
"order": "ترتيب",
|
||||
"pageNotFound": "الصفحة غير موجودة",
|
||||
"order": "التّرتيب",
|
||||
"pageNotFound": "الصّفحة غير موجودة",
|
||||
"password": "كلمة المرور",
|
||||
"payloadSettings": "إعدادات الحمولة",
|
||||
"perPage": "في الصفحة: {{limit}}",
|
||||
"payloadSettings": "الإعدادات",
|
||||
"perPage": "لكلّ صفحة: {{limit}}",
|
||||
"remove": "إزالة",
|
||||
"row": "صف",
|
||||
"rows": "صفوف",
|
||||
"row": "سطر",
|
||||
"rows": "أسطُر",
|
||||
"save": "حفظ",
|
||||
"saving": "جاري الحفظ...",
|
||||
"searchBy": "البحث عن طريق {{label}}",
|
||||
"selectAll": "تحديد كل {{count}} {{label}}",
|
||||
"selectValue": "اختيار قيمة",
|
||||
"selectedCount": "تم تحديد {{count}} {{label}}",
|
||||
"sorryNotFound": "عذرًا - لا يوجد شيء يتوافق مع طلبك.",
|
||||
"saving": "يتمّ الحفظ...",
|
||||
"searchBy": "البحث بواسطة {{label}}",
|
||||
"selectAll": "اختر الكلّ {{count}} {{label}}",
|
||||
"selectValue": "اختر قيمة",
|
||||
"selectedCount": "{{count}} {{label}} تمّ اختيارها",
|
||||
"sorryNotFound": "عذرًا - ليس هناك ما يتوافق مع طلبك.",
|
||||
"sort": "ترتيب",
|
||||
"stayOnThisPage": "البقاء على هذه الصفحة",
|
||||
"submissionSuccessful": "تمت الإرسال بنجاح.",
|
||||
@@ -247,30 +247,30 @@
|
||||
"welcome": "مرحبًا"
|
||||
},
|
||||
"operators": {
|
||||
"contains": "يحتوي على",
|
||||
"contains": "يحتوي",
|
||||
"equals": "يساوي",
|
||||
"exists": "موجود",
|
||||
"isGreaterThan": "أكبر من",
|
||||
"isGreaterThanOrEqualTo": "أكبر من أو يساوي",
|
||||
"isGreaterThanOrEqualTo": "أكبر أو يساوي",
|
||||
"isIn": "موجود في",
|
||||
"isLessThan": "أقل من",
|
||||
"isLessThanOrEqualTo": "أقل من أو يساوي",
|
||||
"isLike": "مشابه لـ",
|
||||
"isLessThan": "أصغر من",
|
||||
"isLessThanOrEqualTo": "أصغر أو يساوي",
|
||||
"isLike": "هو مثل",
|
||||
"isNotEqualTo": "لا يساوي",
|
||||
"isNotIn": "غير موجود في",
|
||||
"near": "قريب من"
|
||||
},
|
||||
"upload": {
|
||||
"dragAndDrop": "اسحب وأفلت الملف",
|
||||
"dragAndDropHere": "أو اسحب وأفلت الملف هنا",
|
||||
"fileName": "اسم الملف",
|
||||
"fileSize": "حجم الملف",
|
||||
"height": "الارتفاع",
|
||||
"lessInfo": "معلومات أقل",
|
||||
"dragAndDrop": "قم بسحب وإسقاط ملفّ",
|
||||
"dragAndDropHere": "أو اسحب الملفّ وأفلته هنا",
|
||||
"fileName": "اسم الملفّ",
|
||||
"fileSize": "حجم الملفّ",
|
||||
"height": "الطّول",
|
||||
"lessInfo": "معلومات أقلّ",
|
||||
"moreInfo": "معلومات أكثر",
|
||||
"selectCollectionToBrowse": "حدد مجموعة لتصفحها",
|
||||
"selectFile": "حدد ملف",
|
||||
"sizes": "الأحجام",
|
||||
"selectCollectionToBrowse": "حدّد مجموعة لاستعراضها",
|
||||
"selectFile": "اختر ملفّ",
|
||||
"sizes": "الاحجام",
|
||||
"width": "العرض"
|
||||
},
|
||||
"validation": {
|
||||
@@ -293,57 +293,57 @@
|
||||
"validUploadID": "هذا الحقل ليس معرّف تحميل صالح."
|
||||
},
|
||||
"version": {
|
||||
"aboutToPublishSelection": "أنت على وشك نشر جميع {{label}} المحددة. هل أنت متأكد؟",
|
||||
"aboutToRestore": "أنت على وشك استعادة هذا الوثيقة {{label}} إلى الحالة التي كانت عليها في {{versionDate}}.",
|
||||
"aboutToRestoreGlobal": "أنت على وشك استعادة {{label}} العالمي إلى الحالة التي كانت عليها في {{versionDate}}.",
|
||||
"aboutToRevertToPublished": "أنت على وشك إرجاع تغييرات هذه الوثيقة إلى الحالة التي كانت عليها عند النشر. هل أنت متأكد؟",
|
||||
"aboutToUnpublish": "أنت على وشك إلغاء نشر هذه الوثيقة. هل أنت متأكد؟",
|
||||
"aboutToUnpublishSelection": "أنت على وشك إلغاء نشر جميع {{label}} المحددة. هل أنت متأكد؟",
|
||||
"aboutToPublishSelection": "أنت على وشك نشر كلّ {{label}} في التّحديد. هل أنت متأكّد؟",
|
||||
"aboutToRestore": "أنت على وشك استرجاع هذا المستند {{label}} إلى الحالة التّي كان عليها في {{versionDate}}.",
|
||||
"aboutToRestoreGlobal": "أنت على وشك استرجاع الاعداد العامّ {{label}} إلى الحالة التي كان عليها في {{versionDate}}.",
|
||||
"aboutToRevertToPublished": "أنت على وشك إعادة هذا المستند إلى حالته المنشورة. هل أنت متأكّد؟",
|
||||
"aboutToUnpublish": "أنت على وشك إلغاء نشر هذا المستند. هل أنت متأكّد؟",
|
||||
"aboutToUnpublishSelection": "أنت على وشك إلغاء نشر كلّ {{label}} في التّحديد. هل أنت متأكّد؟",
|
||||
"autosave": "حفظ تلقائي",
|
||||
"autosavedSuccessfully": "تم الحفظ التلقائي بنجاح.",
|
||||
"autosavedVersion": "نسخة محفوظة تلقائياً",
|
||||
"changed": "تم التغيير",
|
||||
"compareVersion": "مقارنة النسخة مع:",
|
||||
"confirmPublish": "تأكيد النشر",
|
||||
"confirmRevertToSaved": "تأكيد العودة إلى الحالة المحفوظة",
|
||||
"confirmUnpublish": "تأكيد إلغاء النشر",
|
||||
"confirmVersionRestoration": "تأكيد استعادة النسخة",
|
||||
"currentDocumentStatus": "{{docStatus}} الوثيقة الحالية",
|
||||
"draft": "مسودة",
|
||||
"draftSavedSuccessfully": "تم حفظ المسودة بنجاح.",
|
||||
"lastSavedAgo": "تم الحفظ الأخير {{distance, relativetime(minutes)}}",
|
||||
"noFurtherVersionsFound": "لم يتم العثور على مزيد من النسخ",
|
||||
"noRowsFound": "لم يتم العثور على {{label}}",
|
||||
"autosavedSuccessfully": "تمّ الحفظ التّلقائي بنجاح.",
|
||||
"autosavedVersion": "النّسخة المحفوظة تلقائياً",
|
||||
"changed": "تمّ التّغيير",
|
||||
"compareVersion": "مقارنة النّسخة مع:",
|
||||
"confirmPublish": "تأكيد النّشر",
|
||||
"confirmRevertToSaved": "تأكيد الرّجوع للنسخة المنشورة",
|
||||
"confirmUnpublish": "تأكيد إلغاء النّشر",
|
||||
"confirmVersionRestoration": "تأكيد إستعادة النّسخة",
|
||||
"currentDocumentStatus": "المستند {{docStatus}} الحالي",
|
||||
"draft": "مسودّة",
|
||||
"draftSavedSuccessfully": "تمّ حفظ المسودّة بنجاح.",
|
||||
"lastSavedAgo": "آخر حفظ في {{distance, relativetime(minutes)}}",
|
||||
"noFurtherVersionsFound": "لم يتمّ العثور على نسخات أخرى",
|
||||
"noRowsFound": "لم يتمّ العثور على {{label}}",
|
||||
"preview": "معاينة",
|
||||
"problemRestoringVersion": "حدثت مشكلة أثناء استعادة هذا الإصدار",
|
||||
"problemRestoringVersion": "حدث خطأ في استعادة هذه النّسخة",
|
||||
"publish": "نشر",
|
||||
"publishChanges": "نشر التغييرات",
|
||||
"published": "تم النشر",
|
||||
"restoreThisVersion": "استعادة هذا الإصدار",
|
||||
"restoredSuccessfully": "تمت الاستعادة بنجاح.",
|
||||
"restoring": "جارٍ الاستعادة...",
|
||||
"revertToPublished": "العودة إلى النسخة المنشورة",
|
||||
"reverting": "جارٍ العودة...",
|
||||
"saveDraft": "حفظ المسودة",
|
||||
"selectLocales": "تحديد اللغات المراد عرضها",
|
||||
"selectVersionToCompare": "حدد إصدارًا للمقارنة",
|
||||
"showLocales": "عرض اللغات:",
|
||||
"showingVersionsFor": "عرض الإصدارات لـ:",
|
||||
"publishChanges": "نشر التّغييرات",
|
||||
"published": "تمّ النّشر",
|
||||
"restoreThisVersion": "استعادة هذه النّسخة",
|
||||
"restoredSuccessfully": "تمّت الاستعادة بنحاح.",
|
||||
"restoring": "تتمّ الاستعادة...",
|
||||
"revertToPublished": "الرّجوع للنسخة المنشورة",
|
||||
"reverting": "يتمّ الاسترجاع...",
|
||||
"saveDraft": "حفظ المسودّة",
|
||||
"selectLocales": "حدّد اللّغات المراد عرضها",
|
||||
"selectVersionToCompare": "حدّد نسخة للمقارنة",
|
||||
"showLocales": "اظهر اللّغات:",
|
||||
"showingVersionsFor": "يتمّ عرض النًّسخ ل:",
|
||||
"status": "الحالة",
|
||||
"type": "النوع",
|
||||
"unpublish": "إلغاء النشر",
|
||||
"unpublishing": "جارٍ إلغاء النشر...",
|
||||
"version": "نسخة",
|
||||
"versionCount_many": "تم العثور على {{count}} نسخة",
|
||||
"versionCount_none": "لا توجد نسخ",
|
||||
"versionCount_one": "تم العثور على نسخة واحدة",
|
||||
"versionCount_other": "تم العثور على {{count}} نسخة",
|
||||
"versionCreatedOn": "تم إنشاء {{version}} في تاريخ:",
|
||||
"versionID": "معرف النسخة",
|
||||
"versions": "النسخ",
|
||||
"viewingVersion": "عرض نسخة لـ {{entityLabel}} {{documentTitle}}",
|
||||
"viewingVersionGlobal": "عرض نسخة للـ {{entityLabel}} العام",
|
||||
"viewingVersions": "عرض النسخ لـ {{entityLabel}} {{documentTitle}}",
|
||||
"viewingVersionsGlobal": "عرض النسخ للـ {{entityLabel}} العام"
|
||||
"type": "النّوع",
|
||||
"unpublish": "الغاء النّشر",
|
||||
"unpublishing": "يتمّ الغاء النّشر...",
|
||||
"version": "النّسخة",
|
||||
"versionCount_many": "تمّ العثور على {{count}} نُسخ",
|
||||
"versionCount_none": "لم يتمّ العثور على أيّ من النّسخ",
|
||||
"versionCount_one": "تمّ العثور على {{count}} من النّسخ",
|
||||
"versionCount_other": "تمّ العثور على {{count}} نُسخ",
|
||||
"versionCreatedOn": "تمّ ﻹنشاء النّسخة في {{version}}:",
|
||||
"versionID": "مُعرّف النّسخة",
|
||||
"versions": "النُّسَخ",
|
||||
"viewingVersion": "يتمّ استعراض نسخة ل {{entityLabel}} {{documentTitle}}",
|
||||
"viewingVersionGlobal": "يتمّ استعراض نسخة للاعداد العامّ {{entityLabel}}",
|
||||
"viewingVersions": "يتمّ استعراض النُّسَخ ل {{entityLabel}} {{documentTitle}}",
|
||||
"viewingVersionsGlobal": "يتمّ استعراض النُّسَخ للاعداد العامّ {{entityLabel}}"
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user