* Split a BCP 47 language tag into locale and extension.
(tag)
| 213 | * Split a BCP 47 language tag into locale and extension. |
| 214 | */ |
| 215 | function splitLanguageTag(tag) { |
| 216 | // Search for the beginning of one or more extension tags, each of which |
| 217 | // contains a singleton tag followed by one or more subtags. The equivalent |
| 218 | // regexp is: /(-[0-9A-Za-z](-[0-9A-Za-z]{2,8})+)+$/. For example, in |
| 219 | // 'de-DE-u-co-phonebk' the matched extension tags are '-u-co-phonebk'. |
| 220 | // |
| 221 | // The below is a mini-parser that reads backwards from the end of the string. |
| 222 | |
| 223 | function charCode(char) { return char.charCodeAt(0); } |
| 224 | function isAlphaNumeric(code) { |
| 225 | return (charCode("0") <= code && code <= charCode("9")) || |
| 226 | (charCode("A") <= code && code <= charCode("Z")) || |
| 227 | (charCode("a") <= code && code <= charCode("z")); |
| 228 | } |
| 229 | |
| 230 | const MATCH_SUBTAG = 0; |
| 231 | const MATCH_SINGLETON_OR_SUBTAG = 1; |
| 232 | let state = MATCH_SUBTAG; |
| 233 | |
| 234 | const MINIMUM_TAG_LENGTH = 2; |
| 235 | const MAXIMUM_TAG_LENGTH = 8; |
| 236 | let currentTagLength = 0; |
| 237 | |
| 238 | // -1 signifies failure, a non-negative integer is the start index of the |
| 239 | // extension tag. |
| 240 | let extensionTagStartIndex = -1; |
| 241 | |
| 242 | for (let i = tag.length - 1; i >= 0; i--) { |
| 243 | const currentCharCode = tag.charCodeAt(i); |
| 244 | if (currentCharCode == charCode("-")) { |
| 245 | if (state == MATCH_SINGLETON_OR_SUBTAG && currentTagLength == 1) { |
| 246 | // Found the singleton tag, the match succeeded. |
| 247 | // Save the matched index, and reset the state. After this point, we |
| 248 | // definitely have a match, but we may still find another extension tag |
| 249 | // sequence. |
| 250 | extensionTagStartIndex = i; |
| 251 | state = MATCH_SUBTAG; |
| 252 | currentTagLength = 0; |
| 253 | } else if (MINIMUM_TAG_LENGTH <= currentTagLength && |
| 254 | currentTagLength <= MAXIMUM_TAG_LENGTH) { |
| 255 | // Found a valid subtag. |
| 256 | state = MATCH_SINGLETON_OR_SUBTAG; |
| 257 | currentTagLength = 0; |
| 258 | } else { |
| 259 | // Invalid subtag (too short or too long). |
| 260 | break; |
| 261 | } |
| 262 | } else if (isAlphaNumeric(currentCharCode)) { |
| 263 | // An alphanumeric character is potentially part of a tag. |
| 264 | currentTagLength++; |
| 265 | } else { |
| 266 | // Any other character is invalid. |
| 267 | break; |
| 268 | } |
| 269 | } |
| 270 | |
| 271 | if (extensionTagStartIndex != -1) { |
| 272 | return { locale: tag.substring(0, extensionTagStartIndex), |
no test coverage detected
searching dependent graphs…