feat: support multiple country codes for sending SMS (#1557)

* feat: support multiple country code

* feat: improve UI

* feat: migrate accountItem

* fix: Aliyun compatible

* fix: phone validate

* fix: typo
This commit is contained in:
Yaodong Yu
2023-02-16 22:53:28 +08:00
committed by GitHub
parent 4605938f8e
commit eb72c9f273
52 changed files with 560 additions and 258 deletions

View File

@ -15,7 +15,6 @@ describe("Login test", () => {
"password": "123",
"autoSignin": true,
"type": "login",
"phonePrefix": "86",
},
}).then((Response) => {
expect(Response).property("body").property("status").to.equal("ok");
@ -40,7 +39,6 @@ describe("Login test", () => {
"password": "1234",
"autoSignin": true,
"type": "login",
"phonePrefix": "86",
},
}).then((Response) => {
expect(Response).property("body").property("status").to.equal("error");

View File

@ -34,7 +34,6 @@ Cypress.Commands.add('login', ()=>{
"password": "123",
"autoSignin": true,
"type": "login",
"phonePrefix": "86",
},
}).then((Response) => {
expect(Response).property("body").property("status").to.equal("ok");

View File

@ -21,6 +21,7 @@
"file-saver": "^2.0.5",
"i18n-iso-countries": "^7.0.0",
"i18next": "^19.8.9",
"libphonenumber-js": "^1.10.19",
"moment": "^2.29.1",
"qs": "^6.10.2",
"react": "^18.2.0",

View File

@ -78,6 +78,7 @@ class AccountTable extends React.Component {
{name: "Password", displayName: i18next.t("general:Password")},
{name: "Email", displayName: i18next.t("general:Email")},
{name: "Phone", displayName: i18next.t("general:Phone")},
{name: "Country code", displayName: i18next.t("user:Country code")},
{name: "Country/Region", displayName: i18next.t("user:Country/Region")},
{name: "Location", displayName: i18next.t("user:Location")},
{name: "Affiliation", displayName: i18next.t("user:Affiliation")},

View File

@ -184,12 +184,20 @@ class OrganizationEditPage extends React.Component {
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Phone prefix"), i18next.t("general:Phone prefix - Tooltip"))} :
{Setting.getLabel(i18next.t("general:Supported country code"), i18next.t("general:Supported country code - Tooltip"))} :
</Col>
<Col span={22} >
<Input addonBefore={"+"} value={this.state.organization.phonePrefix} onChange={e => {
this.updateOrganizationField("phonePrefix", e.target.value);
}} />
<Select virtual={false} mode={"multiple"} style={{width: "100%"}} value={this.state.organization.countryCodes ?? []}
options={Setting.getCountriesData().map(country => {
return Setting.getOption(
<>
{Setting.countryFlag(country)}
{`${country.name} +${country.phone}`}
</>,
country.code);
})} onChange={value => {
this.updateOrganizationField("countryCodes", value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
@ -257,22 +265,13 @@ class OrganizationEditPage extends React.Component {
</Col>
<Col span={22} >
<Select virtual={false} mode="tags" style={{width: "100%"}}
value={this.state.organization.languages}
options={Setting.Countries.map((item) => {
return Setting.getOption(item.label, item.key);
})}
value={this.state.organization.languages ?? []}
onChange={(value => {
this.updateOrganizationField("languages", value);
})} >
{
[
{value: "en", label: "English"},
{value: "zh", label: "简体中文"},
{value: "es", label: "Español"},
{value: "fr", label: "Français"},
{value: "de", label: "Deutsch"},
{value: "ja", label: "日本語"},
{value: "ko", label: "한국어"},
{value: "ru", label: "Русский"},
].map((item, index) => <Option key={index} value={item.value}>{item.label}</Option>)
}
</Select>
</Col>
</Row>

View File

@ -33,7 +33,7 @@ class OrganizationListPage extends BaseListPage {
favicon: `${Setting.StaticBaseUrl}/img/favicon.png`,
passwordType: "plain",
PasswordSalt: "",
phonePrefix: "86",
countryCodes: ["CN"],
defaultAvatar: `${Setting.StaticBaseUrl}/img/casbin.svg`,
defaultApplication: "",
tags: [],

View File

@ -18,16 +18,6 @@ import {Dropdown} from "antd";
import "./App.less";
import {GlobalOutlined} from "@ant-design/icons";
export const Countries = [{label: "English", key: "en", country: "US", alt: "English"},
{label: "简体中文", key: "zh", country: "CN", alt: "简体中文"},
{label: "Español", key: "es", country: "ES", alt: "Español"},
{label: "Français", key: "fr", country: "FR", alt: "Français"},
{label: "Deutsch", key: "de", country: "DE", alt: "Deutsch"},
{label: "日本語", key: "ja", country: "JP", alt: "日本語"},
{label: "한국어", key: "ko", country: "KR", alt: "한국어"},
{label: "Русский", key: "ru", country: "RU", alt: "Русский"},
];
function flagIcon(country, alt) {
return (
<img width={24} alt={alt} src={`${Setting.StaticBaseUrl}/flag-icons/${country}.svg`} />
@ -39,15 +29,15 @@ class SelectLanguageBox extends React.Component {
super(props);
this.state = {
classes: props,
languages: props.languages ?? ["en", "zh", "es", "fr", "de", "ja", "ko", "ru"],
languages: props.languages ?? Setting.Countries.map(item => item.key),
};
Countries.forEach((country) => {
Setting.Countries.forEach((country) => {
new Image().src = `${Setting.StaticBaseUrl}/flag-icons/${country.country}.svg`;
});
}
items = Countries.map((country) => Setting.getItem(country.label, country.key, flagIcon(country.country, country.alt)));
items = Setting.Countries.map((country) => Setting.getItem(country.label, country.key, flagIcon(country.country, country.alt)));
getOrganizationLanguages(languages) {
const select = [];

View File

@ -49,9 +49,9 @@ class SelectRegionBox extends React.Component {
}
>
{
Setting.getCountryNames().map((item) => (
Setting.getCountriesData().map((item) => (
<Option key={item.code} value={item.code} label={`${item.name} (${item.code})`} >
<img src={`${Setting.StaticBaseUrl}/flag-icons/${item.code}.svg`} alt={item.name} height={20} style={{marginRight: 10}} />
{Setting.countryFlag(item)}
{`${item.name} (${item.code})`}
</Option>
))

View File

@ -23,6 +23,7 @@ import copy from "copy-to-clipboard";
import {authConfig} from "./auth/Auth";
import {Helmet} from "react-helmet";
import * as Conf from "./Conf";
import * as phoneNumber from "libphonenumber-js";
import * as path from "path-browserify";
export const ServerUrl = "";
@ -30,6 +31,16 @@ export const ServerUrl = "";
// export const StaticBaseUrl = "https://cdn.jsdelivr.net/gh/casbin/static";
export const StaticBaseUrl = "https://cdn.casbin.org";
export const Countries = [{label: "English", key: "en", country: "US", alt: "English"},
{label: "简体中文", key: "zh", country: "CN", alt: "简体中文"},
{label: "Español", key: "es", country: "ES", alt: "Español"},
{label: "Français", key: "fr", country: "FR", alt: "Français"},
{label: "Deutsch", key: "de", country: "DE", alt: "Deutsch"},
{label: "日本語", key: "ja", country: "JP", alt: "日本語"},
{label: "한국어", key: "ko", country: "KR", alt: "한국어"},
{label: "Русский", key: "ru", country: "RU", alt: "Русский"},
];
export function getThemeData(organization, application) {
if (application?.themeData?.isEnabled) {
return application.themeData;
@ -188,20 +199,33 @@ export const OtherProviderInfo = {
},
};
export function getCountriesData() {
export function initCountries() {
const countries = require("i18n-iso-countries");
countries.registerLocale(require("i18n-iso-countries/langs/" + getLanguage() + ".json"));
return countries;
}
export function getCountryNames() {
const data = getCountriesData().getNames(getLanguage(), {select: "official"});
return Object.entries(data).map(items => {
return {code: items[0], name: items[1]};
export function getCountriesData(countryCodes = phoneNumber.getCountries()) {
return countryCodes?.map((countryCode) => {
if (phoneNumber.isSupportedCountry(countryCode)) {
const name = initCountries().getName(countryCode, getLanguage());
return {
code: countryCode,
name: name || "",
phone: phoneNumber.getCountryCallingCode(countryCode),
};
}
});
}
export function countryFlag(country) {
return <img src={`${StaticBaseUrl}/flag-icons/${country.code}.svg`} alt={country.name} height={20} style={{marginRight: 10}} />;
}
export function getPhoneCodeFromCountryCode(countryCode) {
return phoneNumber.isSupportedCountry(countryCode) ? phoneNumber.getCountryCallingCode(countryCode) : "";
}
export function initServerUrl() {
// const hostname = window.location.hostname;
// if (hostname === "localhost") {
@ -299,16 +323,14 @@ export function isValidEmail(email) {
return emailRegex.test(email);
}
export function isValidPhone(phone) {
return phone !== "";
export function isValidPhone(phone, countryCode = "") {
if (countryCode !== "") {
return phoneNumber.isValidPhoneNumber(phone, countryCode);
}
// if (phone === "") {
// return false;
// }
//
// // https://learnku.com/articles/31543, `^s*$` filter empty email individually.
// const phoneRegex = /^\s*$|^1(3\d|4[5-9]|5[0-35-9]|6[2567]|7[0-8]|8\d|9[0-35-9])\d{8}$/;
// return phoneRegex.test(phone);
const phoneRegex = /[0-9]{4,15}$/;
return phoneRegex.test(phone);
}
export function isValidInvoiceTitle(invoiceTitle) {

View File

@ -29,6 +29,7 @@ import SelectRegionBox from "./SelectRegionBox";
import WebAuthnCredentialTable from "./WebauthnCredentialTable";
import ManagedAccountTable from "./ManagedAccountTable";
import PropertyTable from "./propertyTable";
import PhoneNumberInput from "./common/PhoneNumberInput";
const {Option} = Select;
@ -286,11 +287,13 @@ class UserEditPage extends React.Component {
<Col style={{paddingRight: "20px"}} span={11} >
{Setting.isLocalAdminUser(this.props.account) ?
(<Input value={this.state.user.email}
style={{width: "280Px"}}
disabled={disabled}
onChange={e => {
this.updateUserField("email", e.target.value);
}} />) :
(<Select virtual={false} value={this.state.user.email}
style={{width: "280Px"}}
options={[Setting.getItem(this.state.user.email, this.state.user.email)]}
disabled={disabled}
onChange={e => {
@ -298,7 +301,7 @@ class UserEditPage extends React.Component {
}} />)
}
</Col>
<Col span={11} >
<Col span={Setting.isMobile() ? 22 : 11} >
{/* backend auto get the current user, so admin can not edit. Just self can reset*/}
{this.isSelf() ? <ResetModal application={this.state.application} disabled={disabled} buttonText={i18next.t("user:Reset Email...")} destType={"email"} /> : null}
</Col>
@ -307,24 +310,37 @@ class UserEditPage extends React.Component {
} else if (accountItem.name === "Phone") {
return (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
<Col style={{marginTop: "5px"}} span={Setting.isMobile() ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Phone"), i18next.t("general:Phone - Tooltip"))} :
</Col>
<Col style={{paddingRight: "20px"}} span={11} >
{Setting.isLocalAdminUser(this.props.account) ?
<Input value={this.state.user.phone} addonBefore={`+${this.state.application?.organizationObj.phonePrefix}`}
disabled={disabled}
onChange={e => {
this.updateUserField("phone", e.target.value);
}} /> :
(<Select virtual={false} value={`+${this.state.application?.organizationObj.phonePrefix} ${this.state.user.phone}`}
options={[Setting.getItem(`+${this.state.application?.organizationObj.phonePrefix} ${this.state.user.phone}`, this.state.user.phone)]}
<Input.Group compact style={{width: "280Px"}}>
<PhoneNumberInput
style={{width: "30%"}}
value={this.state.user.countryCode}
onChange={(value) => {
this.updateUserField("countryCode", value);
}}
countryCodes={this.state.application?.organizationObj.countryCodes}
/>
<Input value={this.state.user.phone}
style={{width: "70%"}}
disabled={disabled}
onChange={e => {
this.updateUserField("phone", e.target.value);
}} />
</Input.Group>
:
(<Select virtual={false} value={this.state.user.phone === "" ? null : `+${Setting.getPhoneCodeFromCountryCode(this.state.user.countryCode)} ${this.state.user.phone}`}
options={this.state.user.phone === "" ? null : [Setting.getItem(`+${Setting.getPhoneCodeFromCountryCode(this.state.user.countryCode)} ${this.state.user.phone}`, this.state.user.phone)]}
disabled={disabled}
style={{width: "280px"}}
onChange={e => {
this.updateUserField("phone", e.target.value);
}} />)}
</Col>
<Col span={11} >
<Col span={Setting.isMobile() ? 24 : 11} >
{this.isSelf() ? (<ResetModal application={this.state.application} disabled={disabled} buttonText={i18next.t("user:Reset Phone...")} destType={"phone"} />) : null}
</Col>
</Row>

View File

@ -49,6 +49,7 @@ class UserListPage extends BaseListPage {
avatar: `${Setting.StaticBaseUrl}/img/casbin.svg`,
email: `${randomName}@example.com`,
phone: Setting.getRandomNumber(),
countryCode: this.state.organization.countryCodes?.length > 0 ? this.state.organization.countryCodes[0] : "",
address: [],
affiliation: "Example Inc.",
tag: "staff",
@ -261,7 +262,7 @@ class UserListPage extends BaseListPage {
sorter: true,
...this.getColumnSearchProps("region"),
render: (text, record, index) => {
return Setting.getCountriesData().getName(record.region, Setting.getLanguage(), {select: "official"});
return Setting.initCountries().getName(record.region, Setting.getLanguage(), {select: "official"});
},
},
{

View File

@ -140,7 +140,6 @@ class ForgetPage extends React.Component {
username: this.state.username,
name: this.state.name,
code: forms.step2.getFieldValue("emailCode"),
phonePrefix: this.getApplicationObj()?.organizationObj.phonePrefix,
type: "login",
}, oAuthParams).then(res => {
if (res.status === "ok") {

View File

@ -189,7 +189,6 @@ class LoginPage extends React.Component {
} else {
values["type"] = this.state.type;
}
values["phonePrefix"] = this.getApplicationObj()?.organizationObj.phonePrefix;
if (oAuthParams !== null) {
values["samlRequest"] = oAuthParams.samlRequest;
@ -204,6 +203,7 @@ class LoginPage extends React.Component {
values["organization"] = this.getApplicationObj().organization;
}
}
postCodeLoginAction(res) {
const application = this.getApplicationObj();
const ths = this;
@ -364,7 +364,8 @@ class LoginPage extends React.Component {
title={i18next.t("application:Sign Up Error")}
subTitle={i18next.t("application:The application does not allow to sign up new account")}
extra={[
<Button type="primary" key="signin" onClick={() => Setting.redirectToLoginPage(application, this.props.history)}>
<Button type="primary" key="signin"
onClick={() => Setting.redirectToLoginPage(application, this.props.history)}>
{
i18next.t("login:Sign In")
}
@ -384,7 +385,9 @@ class LoginPage extends React.Component {
application: application.name,
autoSignin: true,
}}
onFinish={(values) => {this.onFinish(values);}}
onFinish={(values) => {
this.onFinish(values);
}}
style={{width: "300px"}}
size="large"
ref={this.form}
@ -424,7 +427,7 @@ class LoginPage extends React.Component {
{
validator: (_, value) => {
if (this.state.loginMethod === "verificationCode") {
if (this.state.email !== "" && !Setting.isValidEmail(this.state.username) && !Setting.isValidPhone(this.state.username)) {
if (!Setting.isValidEmail(this.state.username) && !Setting.isValidPhone(this.state.username)) {
this.setState({validEmailOrPhone: false});
return Promise.reject(i18next.t("login:The input is not valid Email or Phone!"));
}
@ -444,7 +447,7 @@ class LoginPage extends React.Component {
]}
>
<Input
id = "input"
id="input"
prefix={<UserOutlined className="site-form-item-icon" />}
placeholder={(this.state.loginMethod === "verificationCode") ? i18next.t("login:Email or phone") : i18next.t("login:username, Email or phone")}
disabled={!application.enablePassword}
@ -774,13 +777,18 @@ class LoginPage extends React.Component {
const items = [
{label: i18next.t("login:Password"), key: "password"},
];
application.enableCodeSignin ? items.push({label: i18next.t("login:Verification Code"), key: "verificationCode"}) : null;
application.enableCodeSignin ? items.push({
label: i18next.t("login:Verification Code"),
key: "verificationCode",
}) : null;
application.enableWebAuthn ? items.push({label: i18next.t("login:WebAuthn"), key: "webAuthn"}) : null;
if (application.enableCodeSignin || application.enableWebAuthn) {
return (
<div>
<Tabs items={items} size={"small"} defaultActiveKey="password" onChange={(key) => {this.setState({loginMethod: key});}} centered>
<Tabs items={items} size={"small"} defaultActiveKey="password" onChange={(key) => {
this.setState({loginMethod: key});
}} centered>
</Tabs>
</div>
);
@ -823,7 +831,7 @@ class LoginPage extends React.Component {
<div dangerouslySetInnerHTML={{__html: application.formSideHtml}} />
</div>
<div className="login-form">
<div >
<div>
<div>
{
Setting.renderHelmet(application)

View File

@ -26,6 +26,7 @@ import SelectRegionBox from "../SelectRegionBox";
import CustomGithubCorner from "../CustomGithubCorner";
import SelectLanguageBox from "../SelectLanguageBox";
import {withRouter} from "react-router-dom";
import PhoneNumberInput from "../common/PhoneNumberInput";
const formItemLayout = {
labelCol: {
@ -68,6 +69,7 @@ class SignupPage extends React.Component {
application: null,
email: "",
phone: "",
countryCode: "",
emailCode: "",
phoneCode: "",
validEmail: false,
@ -157,7 +159,6 @@ class SignupPage extends React.Component {
onFinish(values) {
const application = this.getApplicationObj();
values.phonePrefix = application.organizationObj.phonePrefix;
AuthBackend.signup(values)
.then((res) => {
if (res.status === "ok") {
@ -378,35 +379,66 @@ class SignupPage extends React.Component {
} else if (signupItem.name === "Phone") {
return (
<React.Fragment>
<Form.Item
name="phone"
key="phone"
label={i18next.t("general:Phone")}
rules={[
{
required: required,
message: i18next.t("signup:Please input your phone number!"),
},
{
validator: (_, value) => {
if (this.state.phone !== "" && !Setting.isValidPhone(this.state.phone)) {
this.setState({validPhone: false});
return Promise.reject(i18next.t("signup:The input is not valid Phone!"));
}
<Form.Item label={i18next.t("general:Phone")} required>
<Input.Group compact>
<Form.Item
name="countryCode"
key="countryCode"
noStyle
rules={[
{
required: required,
message: i18next.t("signup:Please select your country code!"),
},
{
validator: (_, value) => {
if (this.state.phone !== "" && !Setting.isValidPhone(this.state.phone, this.state.countryCode)) {
this.setState({validPhone: false});
return Promise.reject(i18next.t("signup:The input is not valid Phone!"));
}
this.setState({validPhone: true});
return Promise.resolve();
},
},
]}
>
<Input
style={{
width: "100%",
}}
addonBefore={`+${this.getApplicationObj()?.organizationObj.phonePrefix}`}
onChange={e => this.setState({phone: e.target.value})}
/>
this.setState({validPhone: true});
return Promise.resolve();
},
},
]}
>
<PhoneNumberInput
showSearsh={true}
style={{width: "35%"}}
value={this.state.countryCode}
onChange={(value) => {this.setState({countryCode: value});}}
countryCodes={this.getApplicationObj().organizationObj.countryCodes}
/>
</Form.Item>
<Form.Item
name="phone"
key="phone"
noStyle
rules={[
{
required: required,
message: i18next.t("signup:Please input your phone number!"),
},
{
validator: (_, value) => {
if (this.state.phone !== "" && !Setting.isValidPhone(this.state.phone, this.state.countryCode)) {
this.setState({validPhone: false});
return Promise.reject(i18next.t("signup:The input is not valid Phone!"));
}
this.setState({validPhone: true});
return Promise.resolve();
},
},
]}
>
<Input
style={{width: "65%"}}
onChange={e => this.setState({phone: e.target.value})}
/>
</Form.Item>
</Input.Group>
</Form.Item>
<Form.Item
name="phoneCode"

View File

@ -0,0 +1,58 @@
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import {Select} from "antd";
import * as Setting from "../Setting";
import React from "react";
const {Option} = Select;
export default function PhoneNumberInput(props) {
const {onChange, style, showSearch} = props;
const value = props.value ?? "CN";
const countryCodes = props.countryCodes ?? [];
const handleOnChange = (e) => {
onChange?.(e);
};
return (
<Select
virtual={false}
style={style}
value={value}
dropdownMatchSelectWidth={false}
optionLabelProp={"label"}
showSearch={showSearch}
onChange={handleOnChange}
filterOption={(input, option) =>
(option?.label ?? "").toLowerCase().includes(input.toLowerCase())
}
>
{
Setting.getCountriesData(countryCodes).map((country) => (
<Option key={country.code} value={country.code} label={`+${country.phone}`} >
<div style={{display: "flex", justifyContent: "space-between"}}>
<div>
{Setting.countryFlag(country)}
{`${country.name}`}
</div>
{`+${country.phone}`}
</div>
</Option>
))
}
</Select>
);
}

View File

@ -219,8 +219,6 @@
"Permissions - Tooltip": "Permissions - Tooltip",
"Phone": "Telefon",
"Phone - Tooltip": "Phone",
"Phone prefix": "Telefonpräfix",
"Phone prefix - Tooltip": "Mobile phone number prefix, used to distinguish countries or regions",
"Preview": "Vorschau",
"Preview - Tooltip": "The form in which the password is stored in the database",
"Products": "Products",
@ -252,6 +250,8 @@
"Successfully added": "Successfully added",
"Successfully deleted": "Successfully deleted",
"Successfully saved": "Successfully saved",
"Supported country code": "Supported country code",
"Supported country code - Tooltip": "Supported country code - Tooltip",
"Swagger": "Swagger",
"Sync": "Sync",
"Syncers": "Syncers",
@ -311,10 +311,10 @@
"Or sign in with another account": "Oder melden Sie sich mit einem anderen Konto an",
"Password": "Passwort",
"Password - Tooltip": "Passwort - Tooltip",
"Please input your Email or Phone!": "Please input your Email or Phone!",
"Please input your code!": "Bitte gib deinen Code ein!",
"Please input your password!": "Bitte geben Sie Ihr Passwort ein!",
"Please input your password, at least 6 characters!": "Bitte geben Sie Ihr Passwort ein, mindestens 6 Zeichen!",
"Please input your username, Email or phone!": "Bitte geben Sie Ihren Benutzernamen, E-Mail oder Telefon ein!",
"Redirecting, please wait.": "Redirecting, please wait.",
"Sign In": "Anmelden",
"Sign in with WebAuthn": "Sign in with WebAuthn",
@ -640,6 +640,7 @@
"Please input your last name!": "Please input your last name!",
"Please input your phone number!": "Bitte geben Sie Ihre Telefonnummer ein!",
"Please input your real name!": "Bitte geben Sie Ihren persönlichen Namen ein!",
"Please select your country code!": "Please select your country code!",
"Please select your country/region!": "Bitte wählen Sie Ihr Land/Ihre Region!",
"Terms of Use": "Nutzungsbedingungen",
"The input is not invoice Tax ID!": "The input is not invoice Tax ID!",
@ -726,6 +727,7 @@
"Captcha Verify Failed": "Captcha Verify Failed",
"Captcha Verify Success": "Captcha Verify Success",
"Code Sent": "Code gesendet",
"Country code": "Country code",
"Country/Region": "Land/Region",
"Country/Region - Tooltip": "Country/Region",
"Edit User": "Benutzer bearbeiten",

View File

@ -219,8 +219,6 @@
"Permissions - Tooltip": "Permissions - Tooltip",
"Phone": "Phone",
"Phone - Tooltip": "Phone - Tooltip",
"Phone prefix": "Phone prefix",
"Phone prefix - Tooltip": "Phone prefix - Tooltip",
"Preview": "Preview",
"Preview - Tooltip": "Preview - Tooltip",
"Products": "Products",
@ -252,6 +250,8 @@
"Successfully added": "Successfully added",
"Successfully deleted": "Successfully deleted",
"Successfully saved": "Successfully saved",
"Supported country code": "Supported country code",
"Supported country code - Tooltip": "Supported country code - Tooltip",
"Swagger": "Swagger",
"Sync": "Sync",
"Syncers": "Syncers",
@ -311,10 +311,10 @@
"Or sign in with another account": "Or sign in with another account",
"Password": "Password",
"Password - Tooltip": "Password - Tooltip",
"Please input your Email or Phone!": "Please input your Email or Phone!",
"Please input your code!": "Please input your code!",
"Please input your password!": "Please input your password!",
"Please input your password, at least 6 characters!": "Please input your password, at least 6 characters!",
"Please input your username, Email or phone!": "Please input your username, Email or phone!",
"Redirecting, please wait.": "Redirecting, please wait.",
"Sign In": "Sign In",
"Sign in with WebAuthn": "Sign in with WebAuthn",
@ -640,6 +640,7 @@
"Please input your last name!": "Please input your last name!",
"Please input your phone number!": "Please input your phone number!",
"Please input your real name!": "Please input your real name!",
"Please select your country code!": "Please select your country code!",
"Please select your country/region!": "Please select your country/region!",
"Terms of Use": "Terms of Use",
"The input is not invoice Tax ID!": "The input is not invoice Tax ID!",
@ -726,6 +727,7 @@
"Captcha Verify Failed": "Captcha Verify Failed",
"Captcha Verify Success": "Captcha Verify Success",
"Code Sent": "Code Sent",
"Country code": "Country code",
"Country/Region": "Country/Region",
"Country/Region - Tooltip": "Country/Region - Tooltip",
"Edit User": "Edit User",

View File

@ -219,8 +219,6 @@
"Permissions - Tooltip": "Permisos - Tooltip",
"Phone": "Teléfono",
"Phone - Tooltip": "Teléfono - Tooltip",
"Phone prefix": "Prefijo teléfonico",
"Phone prefix - Tooltip": "Prefijo teléfonico - Tooltip",
"Preview": "Previsualizar",
"Preview - Tooltip": "Previsualizar - Tooltip",
"Products": "Productos",
@ -252,6 +250,8 @@
"Successfully added": "Successfully added",
"Successfully deleted": "Successfully deleted",
"Successfully saved": "Successfully saved",
"Supported country code": "Supported country code",
"Supported country code - Tooltip": "Supported country code - Tooltip",
"Swagger": "Swagger",
"Sync": "Sincronizador",
"Syncers": "Sincronizadores",
@ -311,10 +311,10 @@
"Or sign in with another account": "O inicia sesión con otra cuenta",
"Password": "Contraseña",
"Password - Tooltip": "Contraseña - Tooltip",
"Please input your Email or Phone!": "Please input your Email or Phone!",
"Please input your code!": "¡Por favor ingrese su código!",
"Please input your password!": "¡Por favor ingrese su contraseña!",
"Please input your password, at least 6 characters!": "Su contraseña debe contener al menos 6 caracteres.",
"Please input your username, Email or phone!": "¡Ingrese su nombre de usuario, correo electrónico o teléfono!",
"Redirecting, please wait.": "Redirecting, please wait.",
"Sign In": "Iniciar de sesión",
"Sign in with WebAuthn": "Iniciar de sesión con WebAuthn",
@ -640,6 +640,7 @@
"Please input your last name!": "Por favor, ingrese su apellido!",
"Please input your phone number!": "Por favor, ingrese su número teléfonico!",
"Please input your real name!": "Por favor, ingrese un nombre real!",
"Please select your country code!": "Please select your country code!",
"Please select your country/region!": "Por favor, seleccione su pais/region!",
"Terms of Use": "Términos de Uso",
"The input is not invoice Tax ID!": "El valor ingresado no es un número de identificación fiscal de factura!",
@ -726,6 +727,7 @@
"Captcha Verify Failed": "Fallo la verificación del Captcha",
"Captcha Verify Success": "Captcha verificado con éxito",
"Code Sent": "Código enviado",
"Country code": "Country code",
"Country/Region": "Pais/Región",
"Country/Region - Tooltip": "Pais/Región - Tooltip",
"Edit User": "Editar usuario",

View File

@ -219,8 +219,6 @@
"Permissions - Tooltip": "Permissions - Tooltip",
"Phone": "Téléphone",
"Phone - Tooltip": "Phone",
"Phone prefix": "Préfixe du téléphone",
"Phone prefix - Tooltip": "Mobile phone number prefix, used to distinguish countries or regions",
"Preview": "Aperçu",
"Preview - Tooltip": "The form in which the password is stored in the database",
"Products": "Products",
@ -252,6 +250,8 @@
"Successfully added": "Successfully added",
"Successfully deleted": "Successfully deleted",
"Successfully saved": "Successfully saved",
"Supported country code": "Supported country code",
"Supported country code - Tooltip": "Supported country code - Tooltip",
"Swagger": "Swagger",
"Sync": "Sync",
"Syncers": "Synchronisateurs",
@ -311,10 +311,10 @@
"Or sign in with another account": "Ou connectez-vous avec un autre compte",
"Password": "Mot de passe",
"Password - Tooltip": "Mot de passe - Info-bulle",
"Please input your Email or Phone!": "Please input your Email or Phone!",
"Please input your code!": "Veuillez saisir votre code !",
"Please input your password!": "Veuillez saisir votre mot de passe !",
"Please input your password, at least 6 characters!": "Veuillez entrer votre mot de passe, au moins 6 caractères !",
"Please input your username, Email or phone!": "Veuillez entrer votre nom d'utilisateur, votre e-mail ou votre téléphone!",
"Redirecting, please wait.": "Redirecting, please wait.",
"Sign In": "Se connecter",
"Sign in with WebAuthn": "Sign in with WebAuthn",
@ -640,6 +640,7 @@
"Please input your last name!": "Please input your last name!",
"Please input your phone number!": "Veuillez entrer votre numéro de téléphone!",
"Please input your real name!": "Veuillez entrer votre nom personnel !",
"Please select your country code!": "Please select your country code!",
"Please select your country/region!": "Veuillez sélectionner votre pays/région!",
"Terms of Use": "Conditions d'utilisation",
"The input is not invoice Tax ID!": "The input is not invoice Tax ID!",
@ -726,6 +727,7 @@
"Captcha Verify Failed": "Captcha Verify Failed",
"Captcha Verify Success": "Captcha Verify Success",
"Code Sent": "Code envoyé",
"Country code": "Country code",
"Country/Region": "Pays/Région",
"Country/Region - Tooltip": "Country/Region",
"Edit User": "Editer l'utilisateur",

View File

@ -219,8 +219,6 @@
"Permissions - Tooltip": "Permissions - Tooltip",
"Phone": "電話番号",
"Phone - Tooltip": "Phone",
"Phone prefix": "電話プレフィクス",
"Phone prefix - Tooltip": "Mobile phone number prefix, used to distinguish countries or regions",
"Preview": "プレビュー",
"Preview - Tooltip": "The form in which the password is stored in the database",
"Products": "Products",
@ -252,6 +250,8 @@
"Successfully added": "Successfully added",
"Successfully deleted": "Successfully deleted",
"Successfully saved": "Successfully saved",
"Supported country code": "Supported country code",
"Supported country code - Tooltip": "Supported country code - Tooltip",
"Swagger": "Swagger",
"Sync": "Sync",
"Syncers": "Syncers",
@ -311,10 +311,10 @@
"Or sign in with another account": "または別のアカウントでサインイン",
"Password": "パスワード",
"Password - Tooltip": "パスワード → ツールチップ",
"Please input your Email or Phone!": "Please input your Email or Phone!",
"Please input your code!": "コードを入力してください!",
"Please input your password!": "パスワードを入力してください!",
"Please input your password, at least 6 characters!": "6文字以上でパスワードを入力してください",
"Please input your username, Email or phone!": "ユーザー名、メールアドレスまたは電話番号を入力してください。",
"Redirecting, please wait.": "Redirecting, please wait.",
"Sign In": "サインイン",
"Sign in with WebAuthn": "Sign in with WebAuthn",
@ -640,6 +640,7 @@
"Please input your last name!": "Please input your last name!",
"Please input your phone number!": "電話番号を入力してください!",
"Please input your real name!": "個人名を入力してください!",
"Please select your country code!": "Please select your country code!",
"Please select your country/region!": "あなたの国/地域を選択してください!",
"Terms of Use": "利用規約",
"The input is not invoice Tax ID!": "The input is not invoice Tax ID!",
@ -726,6 +727,7 @@
"Captcha Verify Failed": "Captcha Verify Failed",
"Captcha Verify Success": "Captcha Verify Success",
"Code Sent": "コードを送信しました",
"Country code": "Country code",
"Country/Region": "国/地域",
"Country/Region - Tooltip": "Country/Region",
"Edit User": "ユーザーを編集",

View File

@ -219,8 +219,6 @@
"Permissions - Tooltip": "Permissions - Tooltip",
"Phone": "Phone",
"Phone - Tooltip": "Phone",
"Phone prefix": "Phone prefix",
"Phone prefix - Tooltip": "Mobile phone number prefix, used to distinguish countries or regions",
"Preview": "Preview",
"Preview - Tooltip": "The form in which the password is stored in the database",
"Products": "Products",
@ -252,6 +250,8 @@
"Successfully added": "Successfully added",
"Successfully deleted": "Successfully deleted",
"Successfully saved": "Successfully saved",
"Supported country code": "Supported country code",
"Supported country code - Tooltip": "Supported country code - Tooltip",
"Swagger": "Swagger",
"Sync": "Sync",
"Syncers": "Syncers",
@ -311,10 +311,10 @@
"Or sign in with another account": "Or sign in with another account",
"Password": "Password",
"Password - Tooltip": "Password - Tooltip",
"Please input your Email or Phone!": "Please input your Email or Phone!",
"Please input your code!": "Please input your code!",
"Please input your password!": "Please input your password!",
"Please input your password, at least 6 characters!": "Please input your password, at least 6 characters!",
"Please input your username, Email or phone!": "Please input your username, Email or phone!",
"Redirecting, please wait.": "Redirecting, please wait.",
"Sign In": "Sign In",
"Sign in with WebAuthn": "Sign in with WebAuthn",
@ -640,6 +640,7 @@
"Please input your last name!": "Please input your last name!",
"Please input your phone number!": "Please input your phone number!",
"Please input your real name!": "Please input your real name!",
"Please select your country code!": "Please select your country code!",
"Please select your country/region!": "Please select your country/region!",
"Terms of Use": "Terms of Use",
"The input is not invoice Tax ID!": "The input is not invoice Tax ID!",
@ -726,6 +727,7 @@
"Captcha Verify Failed": "Captcha Verify Failed",
"Captcha Verify Success": "Captcha Verify Success",
"Code Sent": "Code Sent",
"Country code": "Country code",
"Country/Region": "Country/Region",
"Country/Region - Tooltip": "Country/Region",
"Edit User": "Edit User",

View File

@ -219,8 +219,6 @@
"Permissions - Tooltip": "Permissions - Tooltip",
"Phone": "Телефон",
"Phone - Tooltip": "Phone",
"Phone prefix": "Префикс телефона",
"Phone prefix - Tooltip": "Mobile phone number prefix, used to distinguish countries or regions",
"Preview": "Предпросмотр",
"Preview - Tooltip": "The form in which the password is stored in the database",
"Products": "Продукты",
@ -252,6 +250,8 @@
"Successfully added": "Successfully added",
"Successfully deleted": "Successfully deleted",
"Successfully saved": "Successfully saved",
"Supported country code": "Supported country code",
"Supported country code - Tooltip": "Supported country code - Tooltip",
"Swagger": "Swagger",
"Sync": "Sync",
"Syncers": "Синхронизаторы",
@ -311,10 +311,10 @@
"Or sign in with another account": "Или войти с помощью другой учетной записи",
"Password": "Пароль",
"Password - Tooltip": "Пароль - Подсказка",
"Please input your Email or Phone!": "Please input your Email or Phone!",
"Please input your code!": "Пожалуйста, введите ваш код!",
"Please input your password!": "Пожалуйста, введите ваш пароль!",
"Please input your password, at least 6 characters!": "Пожалуйста, введите ваш пароль, по крайней мере 6 символов!",
"Please input your username, Email or phone!": "Пожалуйста, введите ваше имя пользователя, адрес электронной почты или телефон!",
"Redirecting, please wait.": "Redirecting, please wait.",
"Sign In": "Войти",
"Sign in with WebAuthn": "Sign in with WebAuthn",
@ -640,6 +640,7 @@
"Please input your last name!": "Please input your last name!",
"Please input your phone number!": "Пожалуйста, введите ваш номер телефона!",
"Please input your real name!": "Пожалуйста, введите ваше личное имя!",
"Please select your country code!": "Please select your country code!",
"Please select your country/region!": "Пожалуйста, выберите вашу страну/регион!",
"Terms of Use": "Условия использования",
"The input is not invoice Tax ID!": "The input is not invoice Tax ID!",
@ -726,6 +727,7 @@
"Captcha Verify Failed": "Captcha Verify Failed",
"Captcha Verify Success": "Captcha Verify Success",
"Code Sent": "Код отправлен",
"Country code": "Country code",
"Country/Region": "Страна/регион",
"Country/Region - Tooltip": "Country/Region",
"Edit User": "Изменить пользователя",

View File

@ -219,8 +219,6 @@
"Permissions - Tooltip": "权限",
"Phone": "手机号",
"Phone - Tooltip": "手机号",
"Phone prefix": "手机号前缀",
"Phone prefix - Tooltip": "移动电话号码前缀,用于区分国家或地区",
"Preview": "预览",
"Preview - Tooltip": "预览",
"Products": "商品",
@ -252,6 +250,8 @@
"Successfully added": "添加成功",
"Successfully deleted": "删除成功",
"Successfully saved": "保存成功",
"Supported country code": "支持的国家代码",
"Supported country code - Tooltip": "支持发送短信的国家 - Tooltip",
"Swagger": "API文档",
"Sync": "同步",
"Syncers": "同步器",
@ -311,10 +311,10 @@
"Or sign in with another account": "或者,登录其他账号",
"Password": "密码",
"Password - Tooltip": "密码",
"Please input your Email or Phone!": "请输入您的Email或手机号!",
"Please input your code!": "请输入您的验证码!",
"Please input your password!": "请输入您的密码!",
"Please input your password, at least 6 characters!": "请输入您的密码不少于6位",
"Please input your username, Email or phone!": "请输入您的用户名、Email或手机号",
"Redirecting, please wait.": "正在跳转, 请稍等.",
"Sign In": "登录",
"Sign in with WebAuthn": "WebAuthn登录",
@ -640,6 +640,7 @@
"Please input your last name!": "请输入您的姓氏!",
"Please input your phone number!": "请输入您的手机号码!",
"Please input your real name!": "请输入您的姓名!",
"Please select your country code!": "请选择国家代码!",
"Please select your country/region!": "请选择您的国家/地区",
"Terms of Use": "《用户协议》",
"The input is not invoice Tax ID!": "您输入的纳税人识别号有误!",
@ -726,6 +727,7 @@
"Captcha Verify Failed": "验证码校验失败",
"Captcha Verify Success": "验证码校验成功",
"Code Sent": "验证码已发送",
"Country code": "国家代码",
"Country/Region": "国家/地区",
"Country/Region - Tooltip": "国家/地区",
"Edit User": "编辑用户",

View File

@ -7829,6 +7829,11 @@ levn@~0.3.0:
prelude-ls "~1.1.2"
type-check "~0.3.2"
libphonenumber-js@^1.10.19:
version "1.10.20"
resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.10.20.tgz#03c310adf83381eeceb4bd6830442fd14e64964d"
integrity sha512-kQovlKNdLcVzerbTPmJ+Fx4R+7/pYXmPDIllHjg7IxL4X6MsMG7jaT5opfYrBok0uqkByVif//JUR8e11l/V7w==
lilconfig@2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.0.5.tgz#19e57fd06ccc3848fd1891655b5a447092225b25"