Merge branch 'feature/translations' of github.com:ONLYOFFICE/AppServer into feature/translations
This commit is contained in:
commit
90a04bcfc7
@ -48,7 +48,9 @@ namespace Frontend.Translations.Tests
|
|||||||
|
|
||||||
var translationFile = new TranslationFile(path);
|
var translationFile = new TranslationFile(path);
|
||||||
|
|
||||||
translationFile.Translations = jsonTranslation.Properties().Select(p => new TranslationItem(p.Name, (string)p.Value)).ToList();
|
translationFile.Translations = jsonTranslation.Properties()
|
||||||
|
.Select(p => new TranslationItem(p.Name, (string)p.Value))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
TranslationFiles.Add(translationFile);
|
TranslationFiles.Add(translationFile);
|
||||||
}
|
}
|
||||||
@ -56,19 +58,19 @@ namespace Frontend.Translations.Tests
|
|||||||
var javascriptFiles = (from wsPath in Workspaces
|
var javascriptFiles = (from wsPath in Workspaces
|
||||||
let clientDir = Path.Combine(BasePath, wsPath)
|
let clientDir = Path.Combine(BasePath, wsPath)
|
||||||
from file in Directory.EnumerateFiles(clientDir, "*.js", SearchOption.AllDirectories)
|
from file in Directory.EnumerateFiles(clientDir, "*.js", SearchOption.AllDirectories)
|
||||||
where file.Contains("src\\")
|
where !file.Contains("dist\\")
|
||||||
select file)
|
select file)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
javascriptFiles.AddRange(from wsPath in Workspaces
|
javascriptFiles.AddRange(from wsPath in Workspaces
|
||||||
let clientDir = Path.Combine(BasePath, wsPath)
|
let clientDir = Path.Combine(BasePath, wsPath)
|
||||||
from file in Directory.EnumerateFiles(clientDir, "*.jsx", SearchOption.AllDirectories)
|
from file in Directory.EnumerateFiles(clientDir, "*.jsx", SearchOption.AllDirectories)
|
||||||
where file.Contains("src\\")
|
where !file.Contains("dist\\")
|
||||||
select file);
|
select file);
|
||||||
|
|
||||||
JavaScriptFiles = new List<JavaScriptFile>();
|
JavaScriptFiles = new List<JavaScriptFile>();
|
||||||
|
|
||||||
var pattern1 = "[.{\\s\\(]t\\(\\s*[\"\'`]([a-zA-Z0-9_.:_\\$\\s{}/_-]+)[\"\'`]\\s*[\\),]";
|
var pattern1 = "[.{\\s\\(]t\\(\\s*[\"\'`]([a-zA-Z0-9_.:_\\s{}/_-]+)[\"\'`]\\s*[\\),]";
|
||||||
var pattern2 = "i18nKey=\"([a-zA-Z0-9_.-]+)\"";
|
var pattern2 = "i18nKey=\"([a-zA-Z0-9_.-]+)\"";
|
||||||
|
|
||||||
var regexp = new Regex($"({pattern1})|({pattern2})", RegexOptions.Multiline | RegexOptions.ECMAScript);
|
var regexp = new Regex($"({pattern1})|({pattern2})", RegexOptions.Multiline | RegexOptions.ECMAScript);
|
||||||
@ -79,7 +81,11 @@ namespace Frontend.Translations.Tests
|
|||||||
|
|
||||||
var matches = regexp.Matches(jsFileText);
|
var matches = regexp.Matches(jsFileText);
|
||||||
|
|
||||||
var translationKeys = matches.Select(m => m.Groups[2].Value == "" ? m.Groups[4].Value : m.Groups[2].Value).ToList();
|
var translationKeys = matches
|
||||||
|
.Select(m => m.Groups[2].Value == ""
|
||||||
|
? m.Groups[4].Value
|
||||||
|
: m.Groups[2].Value)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
if (!translationKeys.Any())
|
if (!translationKeys.Any())
|
||||||
continue;
|
continue;
|
||||||
@ -117,11 +123,18 @@ namespace Frontend.Translations.Tests
|
|||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var enGroup = groupedByLng.Find(f => f.Lng == "en");
|
var enGroup = groupedByLng.Find(f => f.Lng == "en");
|
||||||
|
var expectedCount = enGroup.Count;
|
||||||
|
|
||||||
foreach (var lng in groupedByLng.Where(g => g.Lng != "en"))
|
var otherLngs = groupedByLng.Where(g => g.Lng != "en");
|
||||||
{
|
|
||||||
Assert.AreEqual(enGroup.Count, lng.Count, "language '{0}' is not equals 'en' by translated files count", lng.Lng);
|
var incompleteList = otherLngs
|
||||||
}
|
.Where(lng => lng.Count != expectedCount)
|
||||||
|
.Select(lng => $"{lng.Lng} (Count={lng.Count})")
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
Assert.AreEqual(0, incompleteList.Count,
|
||||||
|
"languages '{0}' are not equal 'en' (Count= {1}) by translated files count",
|
||||||
|
string.Join(", ", incompleteList), expectedCount);
|
||||||
|
|
||||||
//Assert.AreEqual(true, groupedByLng.All(g => g.Count == enGroup.Count));
|
//Assert.AreEqual(true, groupedByLng.All(g => g.Count == enGroup.Count));
|
||||||
}
|
}
|
||||||
@ -137,17 +150,24 @@ namespace Frontend.Translations.Tests
|
|||||||
Keys = grp
|
Keys = grp
|
||||||
.SelectMany(k => k.Translations)
|
.SelectMany(k => k.Translations)
|
||||||
.OrderByDescending(itm => itm.Key)
|
.OrderByDescending(itm => itm.Key)
|
||||||
|
.ToList()
|
||||||
})
|
})
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var enGroup = groupedByLng.Find(f => f.Lng == "en");
|
var enGroup = groupedByLng.Find(f => f.Lng == "en");
|
||||||
|
|
||||||
var expectedCount = enGroup.Keys.Count();
|
var expectedCount = enGroup.Keys.Count;
|
||||||
|
|
||||||
foreach (var lng in groupedByLng.Where(g => g.Lng != "en"))
|
var otherLngs = groupedByLng.Where(g => g.Lng != "en");
|
||||||
{
|
|
||||||
Assert.AreEqual(expectedCount, lng.Keys.Count(), "language '{0}' is not equals 'en' by translated keys count", lng.Lng);
|
var incompleteList = otherLngs
|
||||||
}
|
.Where(lng => lng.Keys.Count != expectedCount)
|
||||||
|
.Select(lng => $"{lng.Lng} (Count={lng.Keys.Count})")
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
Assert.AreEqual(0, incompleteList.Count,
|
||||||
|
"languages '{0}' are not equal 'en' (Count= {1}) by translated keys count",
|
||||||
|
string.Join(", ", incompleteList), expectedCount);
|
||||||
|
|
||||||
//Assert.AreEqual(true, groupedByLng.All(g => g.Keys.Count() == enGroup.Keys.Count()));
|
//Assert.AreEqual(true, groupedByLng.All(g => g.Keys.Count() == enGroup.Keys.Count()));
|
||||||
}
|
}
|
||||||
@ -167,7 +187,9 @@ namespace Frontend.Translations.Tests
|
|||||||
|
|
||||||
var notFoundJsKeys = allJsTranslationKeys.Except(allEnKeys);
|
var notFoundJsKeys = allJsTranslationKeys.Except(allEnKeys);
|
||||||
|
|
||||||
Assert.AreEqual(0, notFoundJsKeys.Count(), "Some i18n-keys are not exist in translations in 'en' language: Keys: '{0}'", string.Join(", ", notFoundJsKeys));
|
Assert.AreEqual(0, notFoundJsKeys.Count(),
|
||||||
|
"Some i18n-keys are not exist in translations in 'en' language: Keys: '{0}'",
|
||||||
|
string.Join(", ", notFoundJsKeys));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -187,7 +209,9 @@ namespace Frontend.Translations.Tests
|
|||||||
|
|
||||||
var notFoundi18nKeys = allEnKeys.Except(allJsTranslationKeys);
|
var notFoundi18nKeys = allEnKeys.Except(allJsTranslationKeys);
|
||||||
|
|
||||||
Assert.AreEqual(0, notFoundi18nKeys.Count(), "Some i18n-keys are not found in js: Keys: '{0}'", string.Join(", ", notFoundi18nKeys));
|
Assert.AreEqual(0, notFoundi18nKeys.Count(),
|
||||||
|
"Some i18n-keys are not found in js: Keys: '{0}'",
|
||||||
|
string.Join(", ", notFoundi18nKeys));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -110,20 +110,20 @@ export const ConflictResolveType = Object.freeze({
|
|||||||
});
|
});
|
||||||
export const providersData = Object.freeze({
|
export const providersData = Object.freeze({
|
||||||
Google: {
|
Google: {
|
||||||
label: "SignInWithGoogle",
|
label: "Google",
|
||||||
icon: "/static/images/share.google.react.svg",
|
icon: "/static/images/share.google.react.svg",
|
||||||
},
|
},
|
||||||
Facebook: {
|
Facebook: {
|
||||||
label: "SignInWithFacebook",
|
label: "Facebook",
|
||||||
icon: "/static/images/share.facebook.react.svg",
|
icon: "/static/images/share.facebook.react.svg",
|
||||||
},
|
},
|
||||||
Twitter: {
|
Twitter: {
|
||||||
label: "SignInWithTwitter",
|
label: "Twitter",
|
||||||
icon: "/static/images/share.twitter.react.svg",
|
icon: "/static/images/share.twitter.react.svg",
|
||||||
iconOptions: { color: "#2AA3EF" },
|
iconOptions: { color: "#2AA3EF" },
|
||||||
},
|
},
|
||||||
LinkedIn: {
|
LinkedIn: {
|
||||||
label: "SignInWithLinkedIn",
|
label: "LinkedIn",
|
||||||
icon: "/static/images/share.linkedin.react.svg",
|
icon: "/static/images/share.linkedin.react.svg",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -224,3 +224,16 @@ export function toCommunityHostname(hostname) {
|
|||||||
|
|
||||||
return communityHostname;
|
return communityHostname;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getProviderTranslation(provider, t) {
|
||||||
|
switch (provider) {
|
||||||
|
case "Google":
|
||||||
|
return t("SignInWithGoogle");
|
||||||
|
case "Facebook":
|
||||||
|
return t("SignInWithFacebook");
|
||||||
|
case "Twitter":
|
||||||
|
return t("SignInWithTwitter");
|
||||||
|
case "LinkedIn":
|
||||||
|
return t("SignInWithLinkedIn");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -9,7 +9,11 @@ import Link from "@appserver/components/link";
|
|||||||
import ProfileInfo from "./ProfileInfo/ProfileInfo";
|
import ProfileInfo from "./ProfileInfo/ProfileInfo";
|
||||||
import toastr from "studio/toastr";
|
import toastr from "studio/toastr";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { combineUrl, isMe } from "@appserver/common/utils";
|
import {
|
||||||
|
combineUrl,
|
||||||
|
isMe,
|
||||||
|
getProviderTranslation,
|
||||||
|
} from "@appserver/common/utils";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
|
||||||
import { withRouter } from "react-router";
|
import { withRouter } from "react-router";
|
||||||
@ -208,7 +212,7 @@ class SectionBodyContent extends React.PureComponent {
|
|||||||
<FacebookButton
|
<FacebookButton
|
||||||
noHover={true}
|
noHover={true}
|
||||||
iconName={icon}
|
iconName={icon}
|
||||||
label={t(label)}
|
label={getProviderTranslation(label, t)}
|
||||||
className="socialButton"
|
className="socialButton"
|
||||||
$iconOptions={iconOptions}
|
$iconOptions={iconOptions}
|
||||||
/>
|
/>
|
||||||
@ -216,7 +220,7 @@ class SectionBodyContent extends React.PureComponent {
|
|||||||
<SocialButton
|
<SocialButton
|
||||||
noHover={true}
|
noHover={true}
|
||||||
iconName={icon}
|
iconName={icon}
|
||||||
label={t(label)}
|
label={getProviderTranslation(label, t)}
|
||||||
className="socialButton"
|
className="socialButton"
|
||||||
$iconOptions={iconOptions}
|
$iconOptions={iconOptions}
|
||||||
/>
|
/>
|
||||||
|
@ -69,10 +69,6 @@
|
|||||||
"ProductsAndInstruments": "Module und Tools",
|
"ProductsAndInstruments": "Module und Tools",
|
||||||
"ProductUserOpportunities": "Profile und Gruppen anschauen",
|
"ProductUserOpportunities": "Profile und Gruppen anschauen",
|
||||||
"ProjectsProduct": "Projekte",
|
"ProjectsProduct": "Projekte",
|
||||||
"ProjectsUserCapabilityCreate": "Meilensteine, Aufgaben, Diskussionen, Dokumente erstellen und bearbeiten",
|
|
||||||
"ProjectsUserCapabilityForm": "Projektteam bilden und Zugriffsrechte bestimmen",
|
|
||||||
"ProjectsUserCapabilityTrack": "Zeit für Aufgaben verfolgen, Berichte generieren",
|
|
||||||
"ProjectsUserCapabilityView": "Projekte anschauen und an Diskussionen teilnehmen",
|
|
||||||
"RestoreDefaultButton": "Standardmäßige Einstellungen",
|
"RestoreDefaultButton": "Standardmäßige Einstellungen",
|
||||||
"SetDefaultTitle": "Standardtitel setzen",
|
"SetDefaultTitle": "Standardtitel setzen",
|
||||||
"SetPeopleAdmin": "Administrator des Moduls Personen auswählen",
|
"SetPeopleAdmin": "Administrator des Moduls Personen auswählen",
|
||||||
|
@ -78,10 +78,6 @@
|
|||||||
"ProductsAndInstruments": "Modules \u0026 tools",
|
"ProductsAndInstruments": "Modules \u0026 tools",
|
||||||
"ProductUserOpportunities": "View profiles and groups",
|
"ProductUserOpportunities": "View profiles and groups",
|
||||||
"ProjectsProduct": "Projects",
|
"ProjectsProduct": "Projects",
|
||||||
"ProjectsUserCapabilityCreate": "Create and edit milestones, tasks, discussions, documents",
|
|
||||||
"ProjectsUserCapabilityForm": "Form a project team and set access rights",
|
|
||||||
"ProjectsUserCapabilityTrack": "Track time for tasks, generate reports",
|
|
||||||
"ProjectsUserCapabilityView": "View projects and take part in discussions",
|
|
||||||
"RegistrationDate": "Registration date",
|
"RegistrationDate": "Registration date",
|
||||||
"RestoreDefaultButton": "Restore to Default",
|
"RestoreDefaultButton": "Restore to Default",
|
||||||
|
|
||||||
|
@ -78,10 +78,6 @@
|
|||||||
"ProductsAndInstruments": "Модули и инструменты",
|
"ProductsAndInstruments": "Модули и инструменты",
|
||||||
"ProductUserOpportunities": "Просматривать профили и группы",
|
"ProductUserOpportunities": "Просматривать профили и группы",
|
||||||
"ProjectsProduct": "Проекты",
|
"ProjectsProduct": "Проекты",
|
||||||
"ProjectsUserCapabilityCreate": "Создавать и редактировать вехи, задачи, обсуждения, документы",
|
|
||||||
"ProjectsUserCapabilityForm": "Формировать команду проекта и устанавливать права доступа",
|
|
||||||
"ProjectsUserCapabilityTrack": "Учитывать время выполнения задач, генерировать отчеты",
|
|
||||||
"ProjectsUserCapabilityView": "Просматривать проекты и участвовать в обсуждениях",
|
|
||||||
"RegistrationDate": "Дата регистрации",
|
"RegistrationDate": "Дата регистрации",
|
||||||
"RestoreDefaultButton": "Настройки по умолчанию",
|
"RestoreDefaultButton": "Настройки по умолчанию",
|
||||||
"SetDefaultTitle": "Установить заголовок по умолчанию",
|
"SetDefaultTitle": "Установить заголовок по умолчанию",
|
||||||
|
@ -186,7 +186,7 @@ class Form extends React.PureComponent {
|
|||||||
size="big"
|
size="big"
|
||||||
tabIndex={2}
|
tabIndex={2}
|
||||||
label={
|
label={
|
||||||
isLoading ? t("Common:LoadingProcessing") : t("Common:OkButton")
|
isLoading ? t("Common:LoadingProcessing") : t("Common:OKButton")
|
||||||
}
|
}
|
||||||
isDisabled={isLoading}
|
isDisabled={isLoading}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
|
@ -18,7 +18,11 @@ import FacebookButton from "@appserver/components/facebook-button";
|
|||||||
import EmailInput from "@appserver/components/email-input";
|
import EmailInput from "@appserver/components/email-input";
|
||||||
import { getAuthProviders } from "@appserver/common/api/settings";
|
import { getAuthProviders } from "@appserver/common/api/settings";
|
||||||
import PageLayout from "@appserver/common/components/PageLayout";
|
import PageLayout from "@appserver/common/components/PageLayout";
|
||||||
import { combineUrl, createPasswordHash } from "@appserver/common/utils";
|
import {
|
||||||
|
combineUrl,
|
||||||
|
createPasswordHash,
|
||||||
|
getProviderTranslation,
|
||||||
|
} from "@appserver/common/utils";
|
||||||
import { AppServerConfig, providersData } from "@appserver/common/constants";
|
import { AppServerConfig, providersData } from "@appserver/common/constants";
|
||||||
import { isMobile } from "react-device-detect";
|
import { isMobile } from "react-device-detect";
|
||||||
import { desktop } from "@appserver/components/utils/device";
|
import { desktop } from "@appserver/components/utils/device";
|
||||||
@ -182,7 +186,7 @@ class Confirm extends React.PureComponent {
|
|||||||
>
|
>
|
||||||
<FacebookButton
|
<FacebookButton
|
||||||
iconName={icon}
|
iconName={icon}
|
||||||
label={t(label)}
|
label={getProviderTranslation(label, t)}
|
||||||
className="socialButton"
|
className="socialButton"
|
||||||
$iconOptions={iconOptions}
|
$iconOptions={iconOptions}
|
||||||
data-url={faceBookData.url}
|
data-url={faceBookData.url}
|
||||||
@ -213,7 +217,7 @@ class Confirm extends React.PureComponent {
|
|||||||
<div className="buttonWrapper" key={`${item.provider}ProviderItem`}>
|
<div className="buttonWrapper" key={`${item.provider}ProviderItem`}>
|
||||||
<SocialButton
|
<SocialButton
|
||||||
iconName={icon}
|
iconName={icon}
|
||||||
label={t(label)}
|
label={getProviderTranslation(label, t)}
|
||||||
className={`socialButton ${className ? className : ""}`}
|
className={`socialButton ${className ? className : ""}`}
|
||||||
$iconOptions={iconOptions}
|
$iconOptions={iconOptions}
|
||||||
data-url={item.url}
|
data-url={item.url}
|
||||||
|
@ -46,10 +46,10 @@ class Body extends React.PureComponent {
|
|||||||
|
|
||||||
setPaymentsLicense(null, fd)
|
setPaymentsLicense(null, fd)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toastr.success(t("LoadingLicenseSuccess"), "");
|
toastr.success(t("SuccessLoadingLicense"), "");
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
toastr.error(t("LoadingLicenseError"), "LicenseIsNotValid", 0, true);
|
toastr.error(t("ErrorLoadingLicense"), "LicenseIsNotValid", 0, true);
|
||||||
console.log(error);
|
console.log(error);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -45,10 +45,9 @@ const Separator = styled.div`
|
|||||||
class ThirdPartyServices extends React.Component {
|
class ThirdPartyServices extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
const { t } = props;
|
const { t, setDocumentTitle } = props;
|
||||||
document.title = `${t("ThirdPartyAuthorization")} – ${t(
|
|
||||||
"OrganizationName"
|
setDocumentTitle(`${t("ThirdPartyAuthorization")}`);
|
||||||
)}`;
|
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
dialogVisible: false,
|
dialogVisible: false,
|
||||||
@ -203,7 +202,7 @@ ThirdPartyServices.propTypes = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default inject(({ setup, auth }) => {
|
export default inject(({ setup, auth }) => {
|
||||||
const { settingsStore } = auth;
|
const { settingsStore, setDocumentTitle } = auth;
|
||||||
const { urlAuthKeys } = settingsStore;
|
const { urlAuthKeys } = settingsStore;
|
||||||
const {
|
const {
|
||||||
getConsumers,
|
getConsumers,
|
||||||
@ -219,5 +218,6 @@ export default inject(({ setup, auth }) => {
|
|||||||
getConsumers,
|
getConsumers,
|
||||||
updateConsumerProps,
|
updateConsumerProps,
|
||||||
setSelectedConsumer,
|
setSelectedConsumer,
|
||||||
|
setDocumentTitle,
|
||||||
};
|
};
|
||||||
})(withTranslation(["Settings", "Common"])(observer(ThirdPartyServices)));
|
})(withTranslation(["Settings", "Common"])(observer(ThirdPartyServices)));
|
||||||
|
@ -19,7 +19,10 @@ import ForgotPasswordModalDialog from "./sub-components/forgot-password-modal-di
|
|||||||
import Register from "./sub-components/register-container";
|
import Register from "./sub-components/register-container";
|
||||||
import { getAuthProviders } from "@appserver/common/api/settings";
|
import { getAuthProviders } from "@appserver/common/api/settings";
|
||||||
import { checkPwd } from "@appserver/common/desktop";
|
import { checkPwd } from "@appserver/common/desktop";
|
||||||
import { createPasswordHash } from "@appserver/common/utils";
|
import {
|
||||||
|
createPasswordHash,
|
||||||
|
getProviderTranslation,
|
||||||
|
} from "@appserver/common/utils";
|
||||||
import { providersData } from "@appserver/common/constants";
|
import { providersData } from "@appserver/common/constants";
|
||||||
import { inject, observer } from "mobx-react";
|
import { inject, observer } from "mobx-react";
|
||||||
import i18n from "./i18n";
|
import i18n from "./i18n";
|
||||||
@ -350,7 +353,7 @@ const Form = (props) => {
|
|||||||
>
|
>
|
||||||
<FacebookButton
|
<FacebookButton
|
||||||
iconName={icon}
|
iconName={icon}
|
||||||
label={t(label)}
|
label={getProviderTranslation(label, t)}
|
||||||
className="socialButton"
|
className="socialButton"
|
||||||
$iconOptions={iconOptions}
|
$iconOptions={iconOptions}
|
||||||
data-url={faceBookData.url}
|
data-url={faceBookData.url}
|
||||||
@ -380,7 +383,7 @@ const Form = (props) => {
|
|||||||
<div className="buttonWrapper" key={`${item.provider}ProviderItem`}>
|
<div className="buttonWrapper" key={`${item.provider}ProviderItem`}>
|
||||||
<SocialButton
|
<SocialButton
|
||||||
iconName={icon}
|
iconName={icon}
|
||||||
label={t(label)}
|
label={getProviderTranslation(label, t)}
|
||||||
className={`socialButton ${className ? className : ""}`}
|
className={`socialButton ${className ? className : ""}`}
|
||||||
$iconOptions={iconOptions}
|
$iconOptions={iconOptions}
|
||||||
data-url={item.url}
|
data-url={item.url}
|
||||||
|
Loading…
Reference in New Issue
Block a user