Home | History | Annotate | Download | only in locale
      1 /*
      2  * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved.
      3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
      4  *
      5  * This code is free software; you can redistribute it and/or modify it
      6  * under the terms of the GNU General Public License version 2 only, as
      7  * published by the Free Software Foundation.  Oracle designates this
      8  * particular file as subject to the "Classpath" exception as provided
      9  * by Oracle in the LICENSE file that accompanied this code.
     10  *
     11  * This code is distributed in the hope that it will be useful, but WITHOUT
     12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
     13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
     14  * version 2 for more details (a copy is included in the LICENSE file that
     15  * accompanied this code).
     16  *
     17  * You should have received a copy of the GNU General Public License version
     18  * 2 along with this work; if not, write to the Free Software Foundation,
     19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
     20  *
     21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
     22  * or visit www.oracle.com if you need additional information or have any
     23  * questions.
     24  */
     25 
     26 /*
     27  *******************************************************************************
     28  * Copyright (C) 2010, International Business Machines Corporation and         *
     29  * others. All Rights Reserved.                                                *
     30  *******************************************************************************
     31  */
     32 package sun.util.locale;
     33 
     34 import java.util.ArrayList;
     35 import java.util.Collections;
     36 import java.util.HashMap;
     37 import java.util.List;
     38 import java.util.Map;
     39 import java.util.Set;
     40 
     41 public class LanguageTag {
     42     //
     43     // static fields
     44     //
     45     public static final String SEP = "-";
     46     public static final String PRIVATEUSE = "x";
     47     public static final String UNDETERMINED = "und";
     48     public static final String PRIVUSE_VARIANT_PREFIX = "lvariant";
     49 
     50     //
     51     // Language subtag fields
     52     //
     53     private String language = "";      // language subtag
     54     private String script = "";        // script subtag
     55     private String region = "";        // region subtag
     56     private String privateuse = "";    // privateuse
     57 
     58     private List<String> extlangs = Collections.emptyList();   // extlang subtags
     59     private List<String> variants = Collections.emptyList();   // variant subtags
     60     private List<String> extensions = Collections.emptyList(); // extensions
     61 
     62     // Map contains grandfathered tags and its preferred mappings from
     63     // http://www.ietf.org/rfc/rfc5646.txt
     64     // Keys are lower-case strings.
     65     private static final Map<String, String[]> GRANDFATHERED = new HashMap<>();
     66 
     67     static {
     68         // grandfathered = irregular           ; non-redundant tags registered
     69         //               / regular             ; during the RFC 3066 era
     70         //
     71         // irregular     = "en-GB-oed"         ; irregular tags do not match
     72         //               / "i-ami"             ; the 'langtag' production and
     73         //               / "i-bnn"             ; would not otherwise be
     74         //               / "i-default"         ; considered 'well-formed'
     75         //               / "i-enochian"        ; These tags are all valid,
     76         //               / "i-hak"             ; but most are deprecated
     77         //               / "i-klingon"         ; in favor of more modern
     78         //               / "i-lux"             ; subtags or subtag
     79         //               / "i-mingo"           ; combination
     80         //               / "i-navajo"
     81         //               / "i-pwn"
     82         //               / "i-tao"
     83         //               / "i-tay"
     84         //               / "i-tsu"
     85         //               / "sgn-BE-FR"
     86         //               / "sgn-BE-NL"
     87         //               / "sgn-CH-DE"
     88         //
     89         // regular       = "art-lojban"        ; these tags match the 'langtag'
     90         //               / "cel-gaulish"       ; production, but their subtags
     91         //               / "no-bok"            ; are not extended language
     92         //               / "no-nyn"            ; or variant subtags: their meaning
     93         //               / "zh-guoyu"          ; is defined by their registration
     94         //               / "zh-hakka"          ; and all of these are deprecated
     95         //               / "zh-min"            ; in favor of a more modern
     96         //               / "zh-min-nan"        ; subtag or sequence of subtags
     97         //               / "zh-xiang"
     98 
     99         final String[][] entries = {
    100           //{"tag",         "preferred"},
    101             {"art-lojban",  "jbo"},
    102             {"cel-gaulish", "xtg-x-cel-gaulish"},   // fallback
    103             {"en-GB-oed",   "en-GB-x-oed"},         // fallback
    104             {"i-ami",       "ami"},
    105             {"i-bnn",       "bnn"},
    106             {"i-default",   "en-x-i-default"},      // fallback
    107             {"i-enochian",  "und-x-i-enochian"},    // fallback
    108             {"i-hak",       "hak"},
    109             {"i-klingon",   "tlh"},
    110             {"i-lux",       "lb"},
    111             {"i-mingo",     "see-x-i-mingo"},       // fallback
    112             {"i-navajo",    "nv"},
    113             {"i-pwn",       "pwn"},
    114             {"i-tao",       "tao"},
    115             {"i-tay",       "tay"},
    116             {"i-tsu",       "tsu"},
    117             {"no-bok",      "nb"},
    118             {"no-nyn",      "nn"},
    119             {"sgn-BE-FR",   "sfb"},
    120             {"sgn-BE-NL",   "vgt"},
    121             {"sgn-CH-DE",   "sgg"},
    122             {"zh-guoyu",    "cmn"},
    123             {"zh-hakka",    "hak"},
    124             {"zh-min",      "nan-x-zh-min"},        // fallback
    125             {"zh-min-nan",  "nan"},
    126             {"zh-xiang",    "hsn"},
    127         };
    128         for (String[] e : entries) {
    129             GRANDFATHERED.put(LocaleUtils.toLowerString(e[0]), e);
    130         }
    131     }
    132 
    133     private LanguageTag() {
    134     }
    135 
    136     /*
    137      * BNF in RFC5646
    138      *
    139      * Language-Tag  = langtag             ; normal language tags
    140      *               / privateuse          ; private use tag
    141      *               / grandfathered       ; grandfathered tags
    142      *
    143      *
    144      * langtag       = language
    145      *                 ["-" script]
    146      *                 ["-" region]
    147      *                 *("-" variant)
    148      *                 *("-" extension)
    149      *                 ["-" privateuse]
    150      *
    151      * language      = 2*3ALPHA            ; shortest ISO 639 code
    152      *                 ["-" extlang]       ; sometimes followed by
    153      *                                     ; extended language subtags
    154      *               / 4ALPHA              ; or reserved for future use
    155      *               / 5*8ALPHA            ; or registered language subtag
    156      *
    157      * extlang       = 3ALPHA              ; selected ISO 639 codes
    158      *                 *2("-" 3ALPHA)      ; permanently reserved
    159      *
    160      * script        = 4ALPHA              ; ISO 15924 code
    161      *
    162      * region        = 2ALPHA              ; ISO 3166-1 code
    163      *               / 3DIGIT              ; UN M.49 code
    164      *
    165      * variant       = 5*8alphanum         ; registered variants
    166      *               / (DIGIT 3alphanum)
    167      *
    168      * extension     = singleton 1*("-" (2*8alphanum))
    169      *
    170      *                                     ; Single alphanumerics
    171      *                                     ; "x" reserved for private use
    172      * singleton     = DIGIT               ; 0 - 9
    173      *               / %x41-57             ; A - W
    174      *               / %x59-5A             ; Y - Z
    175      *               / %x61-77             ; a - w
    176      *               / %x79-7A             ; y - z
    177      *
    178      * privateuse    = "x" 1*("-" (1*8alphanum))
    179      *
    180      */
    181     public static LanguageTag parse(String languageTag, ParseStatus sts) {
    182         if (sts == null) {
    183             sts = new ParseStatus();
    184         } else {
    185             sts.reset();
    186         }
    187 
    188         StringTokenIterator itr;
    189 
    190         // Check if the tag is grandfathered
    191         String[] gfmap = GRANDFATHERED.get(LocaleUtils.toLowerString(languageTag));
    192         if (gfmap != null) {
    193             // use preferred mapping
    194             itr = new StringTokenIterator(gfmap[1], SEP);
    195         } else {
    196             itr = new StringTokenIterator(languageTag, SEP);
    197         }
    198 
    199         LanguageTag tag = new LanguageTag();
    200 
    201         // langtag must start with either language or privateuse
    202         if (tag.parseLanguage(itr, sts)) {
    203             tag.parseExtlangs(itr, sts);
    204             tag.parseScript(itr, sts);
    205             tag.parseRegion(itr, sts);
    206             tag.parseVariants(itr, sts);
    207             tag.parseExtensions(itr, sts);
    208         }
    209         tag.parsePrivateuse(itr, sts);
    210 
    211         if (!itr.isDone() && !sts.isError()) {
    212             String s = itr.current();
    213             sts.errorIndex = itr.currentStart();
    214             if (s.length() == 0) {
    215                 sts.errorMsg = "Empty subtag";
    216             } else {
    217                 sts.errorMsg = "Invalid subtag: " + s;
    218             }
    219         }
    220 
    221         return tag;
    222     }
    223 
    224     //
    225     // Language subtag parsers
    226     //
    227 
    228     private boolean parseLanguage(StringTokenIterator itr, ParseStatus sts) {
    229         if (itr.isDone() || sts.isError()) {
    230             return false;
    231         }
    232 
    233         boolean found = false;
    234 
    235         String s = itr.current();
    236         if (isLanguage(s)) {
    237             found = true;
    238             language = s;
    239             sts.parseLength = itr.currentEnd();
    240             itr.next();
    241         }
    242 
    243         return found;
    244     }
    245 
    246     private boolean parseExtlangs(StringTokenIterator itr, ParseStatus sts) {
    247         if (itr.isDone() || sts.isError()) {
    248             return false;
    249         }
    250 
    251         boolean found = false;
    252 
    253         while (!itr.isDone()) {
    254             String s = itr.current();
    255             if (!isExtlang(s)) {
    256                 break;
    257             }
    258             found = true;
    259             if (extlangs.isEmpty()) {
    260                 extlangs = new ArrayList<>(3);
    261             }
    262             extlangs.add(s);
    263             sts.parseLength = itr.currentEnd();
    264             itr.next();
    265 
    266             if (extlangs.size() == 3) {
    267                 // Maximum 3 extlangs
    268                 break;
    269             }
    270         }
    271 
    272         return found;
    273     }
    274 
    275     private boolean parseScript(StringTokenIterator itr, ParseStatus sts) {
    276         if (itr.isDone() || sts.isError()) {
    277             return false;
    278         }
    279 
    280         boolean found = false;
    281 
    282         String s = itr.current();
    283         if (isScript(s)) {
    284             found = true;
    285             script = s;
    286             sts.parseLength = itr.currentEnd();
    287             itr.next();
    288         }
    289 
    290         return found;
    291     }
    292 
    293     private boolean parseRegion(StringTokenIterator itr, ParseStatus sts) {
    294         if (itr.isDone() || sts.isError()) {
    295             return false;
    296         }
    297 
    298         boolean found = false;
    299 
    300         String s = itr.current();
    301         if (isRegion(s)) {
    302             found = true;
    303             region = s;
    304             sts.parseLength = itr.currentEnd();
    305             itr.next();
    306         }
    307 
    308         return found;
    309     }
    310 
    311     private boolean parseVariants(StringTokenIterator itr, ParseStatus sts) {
    312         if (itr.isDone() || sts.isError()) {
    313             return false;
    314         }
    315 
    316         boolean found = false;
    317 
    318         while (!itr.isDone()) {
    319             String s = itr.current();
    320             if (!isVariant(s)) {
    321                 break;
    322             }
    323             found = true;
    324             if (variants.isEmpty()) {
    325                 variants = new ArrayList<>(3);
    326             }
    327             variants.add(s);
    328             sts.parseLength = itr.currentEnd();
    329             itr.next();
    330         }
    331 
    332         return found;
    333     }
    334 
    335     private boolean parseExtensions(StringTokenIterator itr, ParseStatus sts) {
    336         if (itr.isDone() || sts.isError()) {
    337             return false;
    338         }
    339 
    340         boolean found = false;
    341 
    342         while (!itr.isDone()) {
    343             String s = itr.current();
    344             if (isExtensionSingleton(s)) {
    345                 int start = itr.currentStart();
    346                 String singleton = s;
    347                 StringBuilder sb = new StringBuilder(singleton);
    348 
    349                 itr.next();
    350                 while (!itr.isDone()) {
    351                     s = itr.current();
    352                     if (isExtensionSubtag(s)) {
    353                         sb.append(SEP).append(s);
    354                         sts.parseLength = itr.currentEnd();
    355                     } else {
    356                         break;
    357                     }
    358                     itr.next();
    359                 }
    360 
    361                 if (sts.parseLength <= start) {
    362                     sts.errorIndex = start;
    363                     sts.errorMsg = "Incomplete extension '" + singleton + "'";
    364                     break;
    365                 }
    366 
    367                 if (extensions.isEmpty()) {
    368                     extensions = new ArrayList<>(4);
    369                 }
    370                 extensions.add(sb.toString());
    371                 found = true;
    372             } else {
    373                 break;
    374             }
    375         }
    376         return found;
    377     }
    378 
    379     private boolean parsePrivateuse(StringTokenIterator itr, ParseStatus sts) {
    380         if (itr.isDone() || sts.isError()) {
    381             return false;
    382         }
    383 
    384         boolean found = false;
    385 
    386         String s = itr.current();
    387         if (isPrivateusePrefix(s)) {
    388             int start = itr.currentStart();
    389             StringBuilder sb = new StringBuilder(s);
    390 
    391             itr.next();
    392             while (!itr.isDone()) {
    393                 s = itr.current();
    394                 if (!isPrivateuseSubtag(s)) {
    395                     break;
    396                 }
    397                 sb.append(SEP).append(s);
    398                 sts.parseLength = itr.currentEnd();
    399 
    400                 itr.next();
    401             }
    402 
    403             if (sts.parseLength <= start) {
    404                 // need at least 1 private subtag
    405                 sts.errorIndex = start;
    406                 sts.errorMsg = "Incomplete privateuse";
    407             } else {
    408                 privateuse = sb.toString();
    409                 found = true;
    410             }
    411         }
    412 
    413         return found;
    414     }
    415 
    416     public static LanguageTag parseLocale(BaseLocale baseLocale, LocaleExtensions localeExtensions) {
    417         LanguageTag tag = new LanguageTag();
    418 
    419         String language = baseLocale.getLanguage();
    420         String script = baseLocale.getScript();
    421         String region = baseLocale.getRegion();
    422         String variant = baseLocale.getVariant();
    423 
    424         boolean hasSubtag = false;
    425 
    426         String privuseVar = null;   // store ill-formed variant subtags
    427 
    428         if (isLanguage(language)) {
    429             // Convert a deprecated language code to its new code
    430             if (language.equals("iw")) {
    431                 language = "he";
    432             } else if (language.equals("ji")) {
    433                 language = "yi";
    434             } else if (language.equals("in")) {
    435                 language = "id";
    436             }
    437             tag.language = language;
    438         }
    439 
    440         if (isScript(script)) {
    441             tag.script = canonicalizeScript(script);
    442             hasSubtag = true;
    443         }
    444 
    445         if (isRegion(region)) {
    446             tag.region = canonicalizeRegion(region);
    447             hasSubtag = true;
    448         }
    449 
    450         // Special handling for no_NO_NY - use nn_NO for language tag
    451         if (tag.language.equals("no") && tag.region.equals("NO") && variant.equals("NY")) {
    452             tag.language = "nn";
    453             variant = "";
    454         }
    455 
    456         if (variant.length() > 0) {
    457             List<String> variants = null;
    458             StringTokenIterator varitr = new StringTokenIterator(variant, BaseLocale.SEP);
    459             while (!varitr.isDone()) {
    460                 String var = varitr.current();
    461                 if (!isVariant(var)) {
    462                     break;
    463                 }
    464                 if (variants == null) {
    465                     variants = new ArrayList<>();
    466                 }
    467                 variants.add(var);  // Do not canonicalize!
    468                 varitr.next();
    469             }
    470             if (variants != null) {
    471                 tag.variants = variants;
    472                 hasSubtag = true;
    473             }
    474             if (!varitr.isDone()) {
    475                 // ill-formed variant subtags
    476                 StringBuilder buf = new StringBuilder();
    477                 while (!varitr.isDone()) {
    478                     String prvv = varitr.current();
    479                     if (!isPrivateuseSubtag(prvv)) {
    480                         // cannot use private use subtag - truncated
    481                         break;
    482                     }
    483                     if (buf.length() > 0) {
    484                         buf.append(SEP);
    485                     }
    486                     buf.append(prvv);
    487                     varitr.next();
    488                 }
    489                 if (buf.length() > 0) {
    490                     privuseVar = buf.toString();
    491                 }
    492             }
    493         }
    494 
    495         List<String> extensions = null;
    496         String privateuse = null;
    497 
    498         if (localeExtensions != null) {
    499             Set<Character> locextKeys = localeExtensions.getKeys();
    500             for (Character locextKey : locextKeys) {
    501                 Extension ext = localeExtensions.getExtension(locextKey);
    502                 if (isPrivateusePrefixChar(locextKey)) {
    503                     privateuse = ext.getValue();
    504                 } else {
    505                     if (extensions == null) {
    506                         extensions = new ArrayList<>();
    507                     }
    508                     extensions.add(locextKey.toString() + SEP + ext.getValue());
    509                 }
    510             }
    511         }
    512 
    513         if (extensions != null) {
    514             tag.extensions = extensions;
    515             hasSubtag = true;
    516         }
    517 
    518         // append ill-formed variant subtags to private use
    519         if (privuseVar != null) {
    520             if (privateuse == null) {
    521                 privateuse = PRIVUSE_VARIANT_PREFIX + SEP + privuseVar;
    522             } else {
    523                 privateuse = privateuse + SEP + PRIVUSE_VARIANT_PREFIX
    524                              + SEP + privuseVar.replace(BaseLocale.SEP, SEP);
    525             }
    526         }
    527 
    528         if (privateuse != null) {
    529             tag.privateuse = privateuse;
    530         }
    531 
    532         if (tag.language.length() == 0 && (hasSubtag || privateuse == null)) {
    533             // use lang "und" when 1) no language is available AND
    534             // 2) any of other subtags other than private use are available or
    535             // no private use tag is available
    536             tag.language = UNDETERMINED;
    537         }
    538 
    539         return tag;
    540     }
    541 
    542     //
    543     // Getter methods for language subtag fields
    544     //
    545 
    546     public String getLanguage() {
    547         return language;
    548     }
    549 
    550     public List<String> getExtlangs() {
    551         if (extlangs.isEmpty()) {
    552             return Collections.emptyList();
    553         }
    554         return Collections.unmodifiableList(extlangs);
    555     }
    556 
    557     public String getScript() {
    558         return script;
    559     }
    560 
    561     public String getRegion() {
    562         return region;
    563     }
    564 
    565     public List<String> getVariants() {
    566         if (variants.isEmpty()) {
    567             return Collections.emptyList();
    568         }
    569         return Collections.unmodifiableList(variants);
    570     }
    571 
    572     public List<String> getExtensions() {
    573         if (extensions.isEmpty()) {
    574             return Collections.emptyList();
    575         }
    576         return Collections.unmodifiableList(extensions);
    577     }
    578 
    579     public String getPrivateuse() {
    580         return privateuse;
    581     }
    582 
    583     //
    584     // Language subtag syntax checking methods
    585     //
    586 
    587     public static boolean isLanguage(String s) {
    588         // language      = 2*3ALPHA            ; shortest ISO 639 code
    589         //                 ["-" extlang]       ; sometimes followed by
    590         //                                     ;   extended language subtags
    591         //               / 4ALPHA              ; or reserved for future use
    592         //               / 5*8ALPHA            ; or registered language subtag
    593         int len = s.length();
    594         return (len >= 2) && (len <= 8) && LocaleUtils.isAlphaString(s);
    595     }
    596 
    597     public static boolean isExtlang(String s) {
    598         // extlang       = 3ALPHA              ; selected ISO 639 codes
    599         //                 *2("-" 3ALPHA)      ; permanently reserved
    600         return (s.length() == 3) && LocaleUtils.isAlphaString(s);
    601     }
    602 
    603     public static boolean isScript(String s) {
    604         // script        = 4ALPHA              ; ISO 15924 code
    605         return (s.length() == 4) && LocaleUtils.isAlphaString(s);
    606     }
    607 
    608     public static boolean isRegion(String s) {
    609         // region        = 2ALPHA              ; ISO 3166-1 code
    610         //               / 3DIGIT              ; UN M.49 code
    611         return ((s.length() == 2) && LocaleUtils.isAlphaString(s))
    612                 || ((s.length() == 3) && LocaleUtils.isNumericString(s));
    613     }
    614 
    615     public static boolean isVariant(String s) {
    616         // variant       = 5*8alphanum         ; registered variants
    617         //               / (DIGIT 3alphanum)
    618         int len = s.length();
    619         if (len >= 5 && len <= 8) {
    620             return LocaleUtils.isAlphaNumericString(s);
    621         }
    622         if (len == 4) {
    623             return LocaleUtils.isNumeric(s.charAt(0))
    624                     && LocaleUtils.isAlphaNumeric(s.charAt(1))
    625                     && LocaleUtils.isAlphaNumeric(s.charAt(2))
    626                     && LocaleUtils.isAlphaNumeric(s.charAt(3));
    627         }
    628         return false;
    629     }
    630 
    631     public static boolean isExtensionSingleton(String s) {
    632         // singleton     = DIGIT               ; 0 - 9
    633         //               / %x41-57             ; A - W
    634         //               / %x59-5A             ; Y - Z
    635         //               / %x61-77             ; a - w
    636         //               / %x79-7A             ; y - z
    637 
    638         return (s.length() == 1)
    639                 && LocaleUtils.isAlphaString(s)
    640                 && !LocaleUtils.caseIgnoreMatch(PRIVATEUSE, s);
    641     }
    642 
    643     public static boolean isExtensionSingletonChar(char c) {
    644         return isExtensionSingleton(String.valueOf(c));
    645     }
    646 
    647     public static boolean isExtensionSubtag(String s) {
    648         // extension     = singleton 1*("-" (2*8alphanum))
    649         int len = s.length();
    650         return (len >= 2) && (len <= 8) && LocaleUtils.isAlphaNumericString(s);
    651     }
    652 
    653     public static boolean isPrivateusePrefix(String s) {
    654         // privateuse    = "x" 1*("-" (1*8alphanum))
    655         return (s.length() == 1)
    656                 && LocaleUtils.caseIgnoreMatch(PRIVATEUSE, s);
    657     }
    658 
    659     public static boolean isPrivateusePrefixChar(char c) {
    660         return (LocaleUtils.caseIgnoreMatch(PRIVATEUSE, String.valueOf(c)));
    661     }
    662 
    663     public static boolean isPrivateuseSubtag(String s) {
    664         // privateuse    = "x" 1*("-" (1*8alphanum))
    665         int len = s.length();
    666         return (len >= 1) && (len <= 8) && LocaleUtils.isAlphaNumericString(s);
    667     }
    668 
    669     //
    670     // Language subtag canonicalization methods
    671     //
    672 
    673     public static String canonicalizeLanguage(String s) {
    674         return LocaleUtils.toLowerString(s);
    675     }
    676 
    677     public static String canonicalizeExtlang(String s) {
    678         return LocaleUtils.toLowerString(s);
    679     }
    680 
    681     public static String canonicalizeScript(String s) {
    682         return LocaleUtils.toTitleString(s);
    683     }
    684 
    685     public static String canonicalizeRegion(String s) {
    686         return LocaleUtils.toUpperString(s);
    687     }
    688 
    689     public static String canonicalizeVariant(String s) {
    690         return LocaleUtils.toLowerString(s);
    691     }
    692 
    693     public static String canonicalizeExtension(String s) {
    694         return LocaleUtils.toLowerString(s);
    695     }
    696 
    697     public static String canonicalizeExtensionSingleton(String s) {
    698         return LocaleUtils.toLowerString(s);
    699     }
    700 
    701     public static String canonicalizeExtensionSubtag(String s) {
    702         return LocaleUtils.toLowerString(s);
    703     }
    704 
    705     public static String canonicalizePrivateuse(String s) {
    706         return LocaleUtils.toLowerString(s);
    707     }
    708 
    709     public static String canonicalizePrivateuseSubtag(String s) {
    710         return LocaleUtils.toLowerString(s);
    711     }
    712 
    713     @Override
    714     public String toString() {
    715         StringBuilder sb = new StringBuilder();
    716 
    717         if (language.length() > 0) {
    718             sb.append(language);
    719 
    720             for (String extlang : extlangs) {
    721                 sb.append(SEP).append(extlang);
    722             }
    723 
    724             if (script.length() > 0) {
    725                 sb.append(SEP).append(script);
    726             }
    727 
    728             if (region.length() > 0) {
    729                 sb.append(SEP).append(region);
    730             }
    731 
    732             for (String variant : variants) {
    733                 sb.append(SEP).append(variant);
    734             }
    735 
    736             for (String extension : extensions) {
    737                 sb.append(SEP).append(extension);
    738             }
    739         }
    740         if (privateuse.length() > 0) {
    741             if (sb.length() > 0) {
    742                 sb.append(SEP);
    743             }
    744             sb.append(privateuse);
    745         }
    746 
    747         return sb.toString();
    748     }
    749 }
    750