Browse Source

JS: libphonenumber 4.1.

pull/567/head
Nikolaos Trogkanis 14 years ago
committed by Mihaela Rosca
parent
commit
3f0793f365
8 changed files with 909 additions and 209 deletions
  1. +2
    -1
      javascript/README
  2. +340
    -92
      javascript/i18n/phonenumbers/metadata.js
  3. +33
    -2
      javascript/i18n/phonenumbers/metadatafortesting.js
  4. +340
    -92
      javascript/i18n/phonenumbers/metadatalite.js
  5. +57
    -0
      javascript/i18n/phonenumbers/phonemetadata.pb.js
  6. +90
    -20
      javascript/i18n/phonenumbers/phonenumberutil.js
  7. +45
    -2
      javascript/i18n/phonenumbers/phonenumberutil_test.js
  8. +2
    -0
      tools/java/java-build/src/com/google/i18n/phonenumbers/BuildMetadataJsonFromXml.java

+ 2
- 1
javascript/README View File

@ -25,7 +25,7 @@ pages with your web browser:
How to update:
==============
The JavaScript library is ported from the Java implementation (revision 348).
The JavaScript library is ported from the Java implementation (revision 374).
When the Java project gets updated follow these steps to update the JavaScript
project:
@ -58,3 +58,4 @@ Port functionality to extract phone-numbers from text (findNumbers).
Port offline phone number geocoder.
Enable PhoneNumberUtil to handle all digits, rather than a subset
(JavaScript has no equivalent to the Java Character.digit).
Port ShortNumberUtil.

+ 340
- 92
javascript/i18n/phonenumbers/metadata.js
File diff suppressed because it is too large
View File


+ 33
- 2
javascript/i18n/phonenumbers/metadatafortesting.js View File

@ -38,6 +38,7 @@ i18n.phonenumbers.metadata.countryCodeToRegionCodeMap = {
,49:["DE"]
,52:["MX"]
,54:["AR"]
,55:["BR"]
,61:["AU"]
,64:["NZ"]
,65:["SG"]
@ -64,6 +65,7 @@ i18n.phonenumbers.metadata.countryToMetadata = {
,"AD",376,"00",,,,,,,1,,,[,,"NA","NA"]
,,,[,,"NA","NA"]
,[,,"NA","NA"]
,,[,,"NA","NA"]
]
,"AO":[,[,,"[29]\\d{8}","\\d{9}"]
,[,,"2\\d(?:[26-9]\\d|\\d[26-9])\\d{5}","\\d{9}",,,"222123456"]
@ -78,6 +80,7 @@ i18n.phonenumbers.metadata.countryToMetadata = {
,,[,,"NA","NA"]
,,,[,,"NA","NA"]
,[,,"NA","NA"]
,,[,,"NA","NA"]
]
,"AR":[,[,,"[1-3689]\\d{9,10}","\\d{6,11}"]
,[,,"[1-3]\\d{9}","\\d{6,10}"]
@ -112,6 +115,7 @@ i18n.phonenumbers.metadata.countryToMetadata = {
,[,,"NA","NA"]
,,,[,,"NA","NA"]
,[,,"NA","NA"]
,,[,,"NA","NA"]
]
,"AU":[,[,,"[1-578]\\d{4,14}","\\d{5,15}"]
,[,,"[2378]\\d{8}","\\d{9}"]
@ -129,6 +133,20 @@ i18n.phonenumbers.metadata.countryToMetadata = {
,,[,,"NA","NA"]
,,,[,,"NA","NA"]
,[,,"NA","NA"]
,,[,,"NA","NA"]
]
,"BR":[,[]
,[]
,[]
,[,,"NA","NA"]
,[,,"NA","NA"]
,[,,"NA","NA"]
,[,,"NA","NA"]
,[,,"NA","NA"]
,"BR",55,"0014","0",,,"0",,,1,,,[,,"NA","NA"]
,,,[,,"NA","NA"]
,[,,"NA","NA"]
,,[,,"19[023]|911","\\d{3}",,,"190"]
]
,"BS":[,[,,"(242|8(00|66|77|88)|900)\\d{7}","\\d{7,10}"]
,[,,"242(?:3(?:02|[236][1-9]|4[0-24-9]|5[0-68]|7[3-57]|9[2-5])|4(?:2[237]|51|64|77)|502|636|702)\\d{4}","\\d{7,10}"]
@ -141,6 +159,7 @@ i18n.phonenumbers.metadata.countryToMetadata = {
,"BS",1,"011","1",,,"1",,,,,,[,,"NA","NA"]
,,,[,,"NA","NA"]
,[,,"NA","NA"]
,,[,,"NA","NA"]
]
,"DE":[,[,,"\\d{4,14}","\\d{2,14}"]
,[,,"(?:[24-6]\\d{2}|3[03-9]\\d|[789](?:[1-9]\\d|0[2-9]))\\d{1,8}","\\d{2,14}",,,"30123456"]
@ -166,6 +185,7 @@ i18n.phonenumbers.metadata.countryToMetadata = {
,,[,,"NA","NA"]
,,,[,,"NA","NA"]
,[,,"NA","NA"]
,,[,,"NA","NA"]
]
,"GB":[,[,,"\\d{10}","\\d{6,10}"]
,[,,"[1-6]\\d{9}","\\d{6,10}"]
@ -187,6 +207,7 @@ i18n.phonenumbers.metadata.countryToMetadata = {
,,[,,"NA","NA"]
,,,[,,"NA","NA"]
,[,,"NA","NA"]
,,[,,"NA","NA"]
]
,"IT":[,[,,"[0389]\\d{5,10}","\\d{6,11}"]
,[,,"0\\d{9,10}","\\d{10,11}"]
@ -208,7 +229,8 @@ i18n.phonenumbers.metadata.countryToMetadata = {
,,[,,"NA","NA"]
,,,[,,"NA","NA"]
,[,,"NA","NA"]
,1]
,1,[,,"NA","NA"]
]
,"JP":[,[]
,[]
,[]
@ -233,6 +255,7 @@ i18n.phonenumbers.metadata.countryToMetadata = {
,,[,,"NA","NA"]
,,,[,,"NA","NA"]
,[,,"NA","NA"]
,,[,,"NA","NA"]
]
,"KR":[,[,,"[1-7]\\d{3,9}|8\\d{8}","\\d{4,10}"]
,[,,"(?:2|[34][1-3]|5[1-5]|6[1-4])(?:1\\d{2,3}|[2-9]\\d{6,7})","\\d{4,10}",,,"22123456"]
@ -270,6 +293,7 @@ i18n.phonenumbers.metadata.countryToMetadata = {
,,[,,"NA","NA"]
,,,[,,"NA","NA"]
,[,,"NA","NA"]
,,[,,"NA","NA"]
]
,"MX":[,[,,"[1-9]\\d{9,10}","\\d{7,11}"]
,[,,"[2-9]\\d{9}","\\d{7,10}"]
@ -304,6 +328,7 @@ i18n.phonenumbers.metadata.countryToMetadata = {
,[,,"NA","NA"]
,,,[,,"NA","NA"]
,[,,"NA","NA"]
,,[,,"NA","NA"]
]
,"NZ":[,[,,"[289]\\d{7,9}|[3-7]\\d{7}","\\d{7,10}"]
,[,,"24099\\d{3}|(?:3[2-79]|[479][2-689]|6[235-9])\\d{6}","\\d{7,8}"]
@ -323,6 +348,7 @@ i18n.phonenumbers.metadata.countryToMetadata = {
,,[,,"NA","NA"]
,,,[,,"NA","NA"]
,[,,"NA","NA"]
,,[,,"NA","NA"]
]
,"PL":[,[,,"[1-9]\\d{8}","\\d{9}"]
,[,,"[1-9]\\d{8}","\\d{9}"]
@ -332,11 +358,12 @@ i18n.phonenumbers.metadata.countryToMetadata = {
,[,,"NA","NA"]
,[,,"NA","NA"]
,[,,"NA","NA"]
,"PL",48,"0~0","0",,,"0",,,,[[,"(\\d{2})(\\d{3})(\\d{2})(\\d{2})","$1 $2 $3 $4",,"0$1",""]
,"PL",48,"00","0",,,"0",,,,[[,"(\\d{2})(\\d{3})(\\d{2})(\\d{2})","$1 $2 $3 $4",,"0$1",""]
]
,,[,,"NA","NA"]
,,,[,,"NA","NA"]
,[,,"NA","NA"]
,,[,,"NA","NA"]
]
,"RE":[,[,,"[268]\\d{8}","\\d{9}"]
,[,,"262\\d{6}","\\d{9}",,,"262161234"]
@ -351,6 +378,7 @@ i18n.phonenumbers.metadata.countryToMetadata = {
,,[,,"NA","NA"]
,,"262|6(?:9[23]|47)|8",[,,"NA","NA"]
,[,,"NA","NA"]
,,[,,"NA","NA"]
]
,"SG":[,[,,"[13689]\\d{7,10}","\\d{8}|\\d{10,11}"]
,[,,"[36]\\d{7}","\\d{8}"]
@ -370,6 +398,7 @@ i18n.phonenumbers.metadata.countryToMetadata = {
,,[,,"NA","NA"]
,,,[,,"NA","NA"]
,[,,"NA","NA"]
,,[,,"NA","NA"]
]
,"US":[,[,,"[13-689]\\d{9}|2[0-35-9]\\d{8}","\\d{7}(?:\\d{3})?",,,"1234567890"]
,[,,"[13-689]\\d{9}|2[0-35-9]\\d{8}","\\d{7}(?:\\d{3})?",,,"1234567890"]
@ -387,6 +416,7 @@ i18n.phonenumbers.metadata.countryToMetadata = {
,[,,"NA","NA"]
,1,,[,,"800\\d{7}","\\d{10}",,,"1234567890"]
,[,,"NA","NA"]
,,[,,"119|911","\\d{3}",,,"911"]
]
,"YT":[,[,,"[268]\\d{8}","\\d{9}"]
,[,,"2696[0-4]\\d{4}","\\d{9}",,,"269601234"]
@ -399,5 +429,6 @@ i18n.phonenumbers.metadata.countryToMetadata = {
,"YT",262,"00","0",,,"0",,,,,,[,,"NA","NA"]
,,"269|639",[,,"NA","NA"]
,[,,"NA","NA"]
,,[,,"NA","NA"]
]
};

+ 340
- 92
javascript/i18n/phonenumbers/metadatalite.js
File diff suppressed because it is too large
View File


+ 57
- 0
javascript/i18n/phonenumbers/phonemetadata.pb.js View File

@ -1019,6 +1019,57 @@ i18n.phonenumbers.PhoneMetadata.prototype.clearUan = function() {
};
/**
* Gets the value of the emergency field.
* @return {i18n.phonenumbers.PhoneNumberDesc} The value.
*/
i18n.phonenumbers.PhoneMetadata.prototype.getEmergency = function() {
return /** @type {i18n.phonenumbers.PhoneNumberDesc} */ (this.get$Value(27));
};
/**
* Gets the value of the emergency field or the default value if not set.
* @return {!i18n.phonenumbers.PhoneNumberDesc} The value.
*/
i18n.phonenumbers.PhoneMetadata.prototype.getEmergencyOrDefault = function() {
return /** @type {!i18n.phonenumbers.PhoneNumberDesc} */ (this.get$ValueOrDefault(27));
};
/**
* Sets the value of the emergency field.
* @param {!i18n.phonenumbers.PhoneNumberDesc} value The value.
*/
i18n.phonenumbers.PhoneMetadata.prototype.setEmergency = function(value) {
this.set$Value(27, value);
};
/**
* @return {boolean} Whether the emergency field has a value.
*/
i18n.phonenumbers.PhoneMetadata.prototype.hasEmergency = function() {
return this.has$Value(27);
};
/**
* @return {number} The number of values in the emergency field.
*/
i18n.phonenumbers.PhoneMetadata.prototype.emergencyCount = function() {
return this.count$Values(27);
};
/**
* Clears the values in the emergency field.
*/
i18n.phonenumbers.PhoneMetadata.prototype.clearEmergency = function() {
this.clear$Field(27);
};
/**
* Gets the value of the no_international_dialling field.
* @return {i18n.phonenumbers.PhoneNumberDesc} The value.
@ -2012,6 +2063,12 @@ goog.proto2.Message.set$Metadata(i18n.phonenumbers.PhoneMetadata, {
fieldType: goog.proto2.Message.FieldType.MESSAGE,
type: i18n.phonenumbers.PhoneNumberDesc
},
27: {
name: 'emergency',
required: true,
fieldType: goog.proto2.Message.FieldType.MESSAGE,
type: i18n.phonenumbers.PhoneNumberDesc
},
24: {
name: 'no_international_dialling',
required: true,


+ 90
- 20
javascript/i18n/phonenumbers/phonenumberutil.js View File

@ -105,13 +105,14 @@ i18n.phonenumbers.PhoneNumberUtil.MIN_LENGTH_FOR_NSN_ = 3;
/**
* The maximum length of the national significant number.
* The ITU says the maximum length should be 15, but we have found longer
* numbers in Germany.
*
* @const
* @type {number}
* @private
*/
i18n.phonenumbers.PhoneNumberUtil.MAX_LENGTH_FOR_NSN_ = 15;
i18n.phonenumbers.PhoneNumberUtil.MAX_LENGTH_FOR_NSN_ = 16;
/**
@ -134,6 +135,17 @@ i18n.phonenumbers.PhoneNumberUtil.MAX_LENGTH_COUNTRY_CODE_ = 3;
i18n.phonenumbers.PhoneNumberUtil.UNKNOWN_REGION_ = 'ZZ';
/**
* The prefix that needs to be inserted in front of a Colombian landline number
* when dialed from a mobile phone in Colombia.
*
* @const
* @type {string}
* @private
*/
i18n.phonenumbers.PhoneNumberUtil.COLOMBIA_MOBILE_TO_FIXED_LINE_PREFIX_ = '3';
/**
* The PLUS_SIGN signifies the international prefix.
*
@ -1390,6 +1402,70 @@ i18n.phonenumbers.PhoneNumberUtil.prototype.
};
/**
* Returns a number formatted in such a way that it can be dialed from a mobile
* phone in a specific region. If the number cannot be reached from the region
* (e.g. some countries block toll-free numbers from being called outside of the
* country), the method returns an empty string.
*
* @param {i18n.phonenumbers.PhoneNumber} number the phone number to be
* formatted.
* @param {string} regionCallingFrom the region where the call is being placed.
* @param {boolean} withFormatting whether the number should be returned with
* formatting symbols, such as spaces and dashes.
* @return {string} the formatted phone number.
*/
i18n.phonenumbers.PhoneNumberUtil.prototype.formatNumberForMobileDialing =
function(number, regionCallingFrom, withFormatting) {
/** @type {?string} */
var regionCode = this.getRegionCodeForNumber(number);
if (!this.isValidRegionCode_(regionCode)) {
return number.hasRawInput() ? number.getRawInputOrDefault() : '';
}
/** @type {string} */
var formattedNumber;
// Clear the extension, as that part cannot normally be dialed together with
// the main number.
/** @type {i18n.phonenumbers.PhoneNumber} */
var numberNoExt = number.clone();
numberNoExt.clearExtension();
/** @type {i18n.phonenumbers.PhoneNumberType} */
var numberType = this.getNumberType(numberNoExt);
if ((regionCode == 'CO') && (regionCallingFrom == 'CO') &&
(numberType == i18n.phonenumbers.PhoneNumberType.FIXED_LINE)) {
formattedNumber = this.formatNationalNumberWithCarrierCode(
numberNoExt,
i18n.phonenumbers.PhoneNumberUtil
.COLOMBIA_MOBILE_TO_FIXED_LINE_PREFIX_);
} else if ((regionCode == 'BR') && (regionCallingFrom == 'BR') &&
((numberType == i18n.phonenumbers.PhoneNumberType.FIXED_LINE) ||
(numberType == i18n.phonenumbers.PhoneNumberType.MOBILE) ||
(numberType == i18n.phonenumbers.PhoneNumberType.FIXED_LINE_OR_MOBILE))) {
formattedNumber = numberNoExt.hasPreferredDomesticCarrierCode() ?
this.formatNationalNumberWithPreferredCarrierCode(numberNoExt, '') :
// Brazilian fixed line and mobile numbers need to be dialed with a
// carrier code when called within Brazil. Without that, most of the
// carriers won't connect the call. Because of that, we return an empty
// string here.
'';
} else if (this.canBeInternationallyDialled(numberNoExt)) {
return withFormatting ?
this.format(numberNoExt,
i18n.phonenumbers.PhoneNumberFormat.INTERNATIONAL) :
this.format(numberNoExt, i18n.phonenumbers.PhoneNumberFormat.E164);
} else {
formattedNumber = (regionCallingFrom == regionCode) ?
this.format(numberNoExt, i18n.phonenumbers.PhoneNumberFormat.NATIONAL) :
'';
}
return withFormatting ?
formattedNumber :
i18n.phonenumbers.PhoneNumberUtil.normalizeDigitsOnly(formattedNumber);
};
/**
* Formats a phone number for out-of-country dialing purposes. If no
* regionCallingFrom is supplied, we format the number in its INTERNATIONAL
@ -1487,7 +1563,9 @@ i18n.phonenumbers.PhoneNumberUtil.prototype.formatOutOfCountryCallingNumber =
* Formats a phone number using the original phone number format that the number
* is parsed from. The original format is embedded in the country_code_source
* field of the PhoneNumber object passed in. If such information is missing,
* the number will be formatted into the NATIONAL format by default.
* the number will be formatted into the NATIONAL format by default. When the
* number is an invalid number, the method returns the raw input when it is
* available.
*
* @param {i18n.phonenumbers.PhoneNumber} number the phone number that needs to
* be formatted in its original number format.
@ -1498,6 +1576,9 @@ i18n.phonenumbers.PhoneNumberUtil.prototype.formatOutOfCountryCallingNumber =
i18n.phonenumbers.PhoneNumberUtil.prototype.formatInOriginalFormat =
function(number, regionCallingFrom) {
if (number.hasRawInput() && !this.isValidNumber(number)) {
return number.getRawInputOrDefault();
}
if (!number.hasCountryCodeSource()) {
return this.format(number, i18n.phonenumbers.PhoneNumberFormat.NATIONAL);
}
@ -1649,16 +1730,11 @@ i18n.phonenumbers.PhoneNumberUtil.prototype.
i18n.phonenumbers.PhoneNumberUtil.prototype.getNationalSignificantNumber =
function(number) {
// The leading zero in the national (significant) number of an Italian phone
// number has a special meaning. Unlike the rest of the world, it indicates
// the number is a landline number. There have been plans to migrate landline
// numbers to start with the digit two since December 2000, but it has not yet
// happened. See http://en.wikipedia.org/wiki/%2B39 for more details. Other
// regions such as Cote d'Ivoire and Gabon use this for their mobile numbers.
// If a leading zero has been set, we prefix this now. Note this is not a
// national prefix.
/** @type {string} */
var nationalNumber = '' + number.getNationalNumber();
if (number.hasItalianLeadingZero() && number.getItalianLeadingZero() &&
this.isLeadingZeroPossible(number.getCountryCodeOrDefault())) {
if (number.hasItalianLeadingZero() && number.getItalianLeadingZero()) {
return '0' + nationalNumber;
}
return nationalNumber;
@ -2720,8 +2796,9 @@ i18n.phonenumbers.PhoneNumberUtil.prototype.parsePrefixAsIdd_ =
if (matchedGroups && matchedGroups[1] != null &&
matchedGroups[1].length > 0) {
/** @type {string} */
var normalizedGroup = i18n.phonenumbers.PhoneNumberUtil.normalizeDigitsOnly(
matchedGroups[1]);
var normalizedGroup =
i18n.phonenumbers.PhoneNumberUtil.normalizeDigitsOnly(
matchedGroups[1]);
if (normalizedGroup == '0') {
return false;
}
@ -2771,13 +2848,6 @@ i18n.phonenumbers.PhoneNumberUtil.prototype.
// Attempt to parse the first digits as an international prefix.
/** @type {!RegExp} */
var iddPattern = new RegExp(possibleIddPrefix);
if (this.parsePrefixAsIdd_(iddPattern, number)) {
i18n.phonenumbers.PhoneNumberUtil.normalizeSB_(number);
return i18n.phonenumbers.PhoneNumber.CountryCodeSource.FROM_NUMBER_WITH_IDD;
}
// If still not found, then try and normalize the number and then try again.
// This shouldn't be done before, since non-numeric characters (+ and ~) may
// legally be in the international prefix.
i18n.phonenumbers.PhoneNumberUtil.normalizeSB_(number);
return this.parsePrefixAsIdd_(iddPattern, number) ?
i18n.phonenumbers.PhoneNumber.CountryCodeSource.FROM_NUMBER_WITH_IDD :


+ 45
- 2
javascript/i18n/phonenumbers/phonenumberutil_test.js View File

@ -622,7 +622,7 @@ function testFormatOutOfCountryCallingNumber() {
assertEquals('1 650 253 0000',
phoneUtil.formatOutOfCountryCallingNumber(US_NUMBER, RegionCode.BS));
assertEquals('0~0 1 650 253 0000',
assertEquals('00 1 650 253 0000',
phoneUtil.formatOutOfCountryCallingNumber(US_NUMBER, RegionCode.PL));
assertEquals('011 44 7912 345 678',
@ -825,6 +825,35 @@ function testFormatWithPreferredCarrierCode() {
phoneUtil.formatNationalNumberWithPreferredCarrierCode(usNumber, '15'));
}
function testFormatNumberForMobileDialing() {
// US toll free numbers are marked as noInternationalDialling in the test
// metadata for testing purposes.
assertEquals('800 253 0000',
phoneUtil.formatNumberForMobileDialing(US_TOLLFREE, RegionCode.US, true));
assertEquals('',
phoneUtil.formatNumberForMobileDialing(US_TOLLFREE, RegionCode.CN, true));
assertEquals('+1 650 253 0000',
phoneUtil.formatNumberForMobileDialing(US_NUMBER, RegionCode.US, true));
/** @type {i18n.phonenumbers.PhoneNumber} */
var usNumberWithExtn = US_NUMBER.clone();
usNumberWithExtn.setExtension('1234');
assertEquals('+1 650 253 0000',
phoneUtil.formatNumberForMobileDialing(usNumberWithExtn,
RegionCode.US, true));
assertEquals('8002530000',
phoneUtil.formatNumberForMobileDialing(US_TOLLFREE,
RegionCode.US, false));
assertEquals('',
phoneUtil.formatNumberForMobileDialing(US_TOLLFREE,
RegionCode.CN, false));
assertEquals('+16502530000',
phoneUtil.formatNumberForMobileDialing(US_NUMBER, RegionCode.US, false));
assertEquals('+16502530000',
phoneUtil.formatNumberForMobileDialing(usNumberWithExtn,
RegionCode.US, false));
}
function testFormatByPattern() {
var PNF = i18n.phonenumbers.PhoneNumberFormat;
/** @type {i18n.phonenumbers.NumberFormat} */
@ -943,6 +972,20 @@ function testFormatUsingOriginalNumberFormat() {
var number5 = phoneUtil.parse('+442087654321', RegionCode.GB);
assertEquals('(020) 8765 4321',
phoneUtil.formatInOriginalFormat(number5, RegionCode.GB));
// Invalid numbers should be formatted using its raw input when that is
// available. Note area codes starting with 7 are intentionally excluded in
// the test metadata for testing purposes.
/** @type {i18n.phonenumbers.PhoneNumber} */
var number6 = phoneUtil.parseAndKeepRawInput('7345678901', RegionCode.US);
assertEquals('7345678901',
phoneUtil.formatInOriginalFormat(number6, RegionCode.US));
// When the raw input is unavailable, format as usual.
/** @type {i18n.phonenumbers.PhoneNumber} */
var number7 = phoneUtil.parse('7345678901', RegionCode.US);
assertEquals('734 567 8901',
phoneUtil.formatInOriginalFormat(number7, RegionCode.US));
}
function testIsPremiumRate() {
@ -1255,7 +1298,7 @@ function testIsPossibleNumberWithReason() {
assertEquals(VR.TOO_SHORT,
phoneUtil.isPossibleNumberWithReason(adNumber));
adNumber.setCountryCode(376);
adNumber.setNationalNumber(1234567890123456);
adNumber.setNationalNumber(12345678901234567);
assertEquals(VR.TOO_LONG,
phoneUtil.isPossibleNumberWithReason(adNumber));
}


+ 2
- 0
tools/java/java-build/src/com/google/i18n/phonenumbers/BuildMetadataJsonFromXml.java View File

@ -353,6 +353,8 @@ public class BuildMetadataJsonFromXml extends Command {
} else {
jsArrayBuilder.append(null);
}
// required PhoneNumberDesc emergency = 27;
toJsArray(metadata.getEmergency(), jsArrayBuilder);
jsArrayBuilder.endArray();
}


Loading…
Cancel
Save