openjdk.ruOpenJDK на русском

JEP 252: Use CLDR Locale Data by Default

Использование данных локалей CLDR по умолчанию

АвторNaoto Sato & Alex Buckley
ОтветственныйNaoto Sato
ТипFeature
ОбластьJDK
СтатусClosed / Delivered
Выпуск9
Компонентcore-libs / java.util:i18n
Обсуждениеi18n dash dev at openjdk dot java dot net
ТрудоёмкостьM
ДлительностьM
РецензентыAlan Bateman, Brian Goetz, Mark Reinhold
ОдобренBrian Goetz
Создан2014/05/20 17:14
Обновлён2025/11/07 14:13
Задача8043554

Аннотация

Использовать данные локалей из Common Locale Data Repository (CLDR) для форматирования дат, времени, валют, языков, стран и часовых поясов в стандартных API Java. CLDR поддерживает Unicode Consortium. Его данные локалей качественнее унаследованных данных в JDK 8. Переход на данные локалей CLDR, а в будущем и изменения данных локалей CLDR могут затронуть приложения, зависящие от локали.

История

Исходный текст этого JEP, написанный в 2014 году для JDK 9, не давал разработчикам достаточных рекомендаций. В 2024 году мы переписали его, чтобы добавить такие рекомендации и сведения о JDK 21 и JDK 23.

Цели

  • Постоянно поддерживать в платформе Java отраслевые стандарты локализации.

  • Обеспечить, чтобы API Java, зависящие от локали, могли работать с современными интернационализированными данными, например с названиями стран и часовых поясов.

  • Предоставить путь миграции для приложений, которые не могут сразу перейти на данные локалей CLDR.

Что не является целью

  • Цель не в том, чтобы каждое локализованное приложение работало на JDK 9 без изменений.

  • Цель не в том, чтобы удалить унаследованные данные локалей JDK в JDK 9.

  • Цель не в том, чтобы обязать все реализации платформы Java использовать одни и те же данные локалей CLDR.

Мотивация

Платформа Java предлагает API, которые помогают локализовать программы на Java, т. е. адаптировать их к разным языкам и странам. Эти API, в основном из пакетов java.text и java.util, зависят от локали: они используют Locale, который настраивает операцию под конкретный язык, страну, календарную систему и другие культурные нормы. С каждой локалью связаны данные локали, которые описывают, как представляются даты, время, валюты, языки, страны и часовые пояса. В примере ниже слова «Thursday» и «March», а также шаблон «EEEE, MMMM, d, y» берутся из данных локали для Locale.US, а «木曜日» и шаблон «y年M月d日EEEE» — из данных локали для Locale.JAPAN:

jshell> Date today = new Date();
today ==> Thu Mar 14 09:49:43 PDT 2024

jshell> import java.text.*;

jshell> DateFormat.getDateInstance(DateFormat.FULL, Locale.US).format(today);
$2 ==> "Thursday, March 14, 2024"

jshell> DateFormat.getDateInstance(DateFormat.FULL, Locale.JAPAN).format(today);
$3 ==> "2024年3月14日木曜日"

JDK 8 содержит данные примерно для 160 локалей. Изначально их создали в 1990-х годах Sun Microsystems и её отраслевые партнёры. Для своего времени эти данные локалей были передовыми, но у них есть ряд проблем:

  • Данные локалей, как и данные о часовых поясах, по своей природе привязаны к постоянно развивающимся международным стандартам, например к списку названий стран ISO. Поддерживать соответствие данных локалей JDK этим стандартам трудоёмко.

  • Данные локалей должны быть расширяемыми, чтобы поддерживать новые форматы даты и времени, новые валюты, новые часовые пояса и т. д. Данные локалей JDK не расширяемы, поэтому поддержка, например, нового формата даты из месяца и года требует дорогостоящих изменений в API Java.

  • Большинство платформ, созданных в 1990-х годах, начинали, по сути, с тех же данных локалей, что и JDK, но со временем разработчики каждой платформы исправляли и дополняли свои данные локалей по-разному. Например, в JDK добавили собственные сокращения названий некоторых часовых поясов. Такие своеобразные изменения могут вызывать проблемы при обмене информацией между локализованными приложениями на разных платформах.

Unicode Consortium создал Common Locale Data Repository (CLDR) в 2003 году, чтобы решить проблемы качества и расширяемости данных локалей. CLDR содержит данные более чем для 500 локалей. Он выходит каждые шесть месяцев, чтобы не отставать от региональных и культурных изменений, а изменения его содержимого проходят через формальный публичный процесс. Данные локалей описываются на предметно-ориентированном языке разметки LDML, благодаря которому CLDR хорошо структурирован и расширяем. В результате CLDR используют все основные операционные системы.

JDK 8 стал первым выпуском Java, который содержал данные локалей CLDR наряду с унаследованными данными локалей 1990-х годов, хотя по умолчанию использовал унаследованные данные. Учитывая высокое качество и широкое распространение CLDR, вся экосистема Java выиграла бы, если бы JDK 9 по умолчанию перешёл на данные локалей CLDR. Продолжать использовать в JDK собственные унаследованные данные локалей, когда есть CLDR как лучшая альтернатива, и нереалистично, и невыгодно. JDK 9 и последующие выпуски по-прежнему будут содержать унаследованные данные локалей, чтобы облегчить миграцию локализованных приложений.

Описание

В JDK 8 и более поздних версиях есть два встроенных поставщика данных локалей: JRE, который предоставляет унаследованные данные локалей 1990-х годов, и CLDR, который предоставляет данные локалей CLDR от Unicode Consortium.

JDK 8 по умолчанию выбирает во время выполнения только поставщика JRE, поэтому API Java, зависящие от локали, используют только унаследованные данные локалей.

JDK 9 по умолчанию будет отдавать во время выполнения приоритет поставщику CLDR, поэтому API Java, зависящие от локали, будут предпочитать данные локалей CLDR унаследованным данным локалей.

Использование данных локалей CLDR — особенность реализации JDK 9, Java Platform Specification (спецификация платформы Java) этого не требует. Другие реализации платформы не обязаны использовать данные локалей CLDR по умолчанию и даже не обязаны предоставлять их как вариант. Такой подход соответствует тому, как платформа Java работает в других областях интернационализации, например при обработке часовых поясов (см. ниже).

Независимо от поставщика данные локали страны US, языковой локали ENGLISH и технической корневой локали находятся в модуле java.base, а все остальные данные локалей — в модуле jdk.localedata. Разработчики, которые собирают собственные образы среды выполнения с помощью инструмента jlink, могут сэкономить место, выбрав, какие локали включить в образ среды выполнения.

Где используются данные локалей

Приложения представляют даты, время, валюты, языки, страны и часовые пояса объектами следующих классов:

  • java.time: Instant, LocalDate, LocalTime, LocalDateTime, ZonedDateTime, ZoneId
  • java.util: Calendar, Currency, Date, TimeZone

API, зависящие от локали, преобразуют эти объекты в строки и обратно, чтобы дату, время, валюту, язык, страну или часовой пояс можно было записать обычным текстом. API используют данные локалей в обоих направлениях: для преобразования объекта в строку (форматирование) и для преобразования строки в объект (разбор). Поведение этих API по умолчанию изменится после перехода на данные локалей CLDR.

Классы Calendar, Currency и TimeZone из пакета java.util по своей природе зависят от локали, потому что их экземпляры создаются для конкретной локали. Они предоставляют методы форматирования и разбора, которые используют данные именно этой локали. Напротив, java.util.Date и шесть классов из пакета java.time не зависят от локали, потому что их экземпляры создаются без привязки к конкретной локали. API, зависящий от локали, для них предоставляют сопутствующие классы: например, класс java.text.DateFormat отвечает за форматирование и разбор объектов Date. Некоторые универсальные классы ввода-вывода тоже предоставляют зависящие от локали API для форматирования. Вот сопутствующие классы и классы ввода-вывода, которые предоставляют API, зависящие от локали:

  • java.io: PrintStream, PrintWriter
  • java.text: BreakIterator, Collator, DateFormat, DateFormatSymbols, DecimalFormatSymbols, NumberFormat
  • java.time.format: DateTimeFormatter
  • java.util: Formatter, Scanner

Некоторые API, критически важные для локализации, не зависят от локали, и поэтому переход на данные локалей CLDR их не затрагивает:

  • java.util.Locale объявляет константы для различных языков и стран, например язык ENGLISH и страну UK. Переход на данные локалей CLDR не затрагивает ни сами константы, ни их строковые представления.

  • java.util.ResourceBundle предоставляет приложениям данные, специфичные для локали, но собственных методов форматирования или разбора не имеет.

  • У java.util.Date есть метод toString(), результат которого намеренно не зависит от локали, как и у одноимённых методов в java.time.LocalDate, java.time.LocalDateTime и т. д.

Как данные локалей CLDR влияют на приложения

Приложения, которые рассчитывают, что API, зависящие от локали, используют унаследованные данные локалей, получат другие результаты при форматировании и, возможно, исключения при разборе, когда эти API в JDK 9 будут использовать данные локалей CLDR.

Перечислить все различия между унаследованными данными локалей и данными локалей CLDR на практике невозможно, но вот семь заметных различий, которые будут видны приложениям (порядок в этом списке ничего не говорит о важности):

  • Локаль страны UK: разделитель компонентов даты — дефис в JRE, но пробел в CLDR.

  • Языковая локаль ENGLISH (страны, где используется английский язык, например UK, US и CANADA):

    • Разделитель между датой и временем — пробел в JRE, но запятая в CLDR.

    • Полные названия часовых поясов различаются: в JRE они сокращены, а в CLDR нет. Например, PDT в JRE, но Pacific Daylight Time в CLDR.

    • Значение NaN представлено как (символ замены Unicode U+FFFD) в JRE, но как NaN в CLDR.

  • Локаль страны GERMANY: сокращённые названия месяцев (кроме мая) различаются. Это Jan, Feb, Mär, Apr, Jun, Jul, Aug, Sep, Okt, Nov, Dez в JRE, но Jan., Feb., März, Apr., Juni, Juli, Aug., Sep., Okt., Nov., Dez. в CLDR.

  • Локаль страны ITALY: символ валюты (EURO) ставится перед денежной суммой в JRE, но после неё в CLDR.

  • Языковая локаль FRENCH: литовский язык называется lithuanien в JRE, но lituanien в CLDR.

Вот примеры этих различий:

System.out.println(DateFormat.getDateInstance(DateFormat.MEDIUM, Locale.UK)
                             .format(new Date()));
// JDK 8:  15-Mar-2024
// JDK 9:  15 Mar 2024

System.out.println(DateFormat.getDateTimeInstance(DateFormat.SHORT,
                                                  DateFormat.SHORT,
                                                  Locale.ENGLISH)
                             .format(new Date()));
// JDK 8:  3/19/24 2:35 PM
// JDK 9:  3/19/24, 2:35 PM

System.out.println(DateFormat.getTimeInstance(DateFormat.FULL, Locale.ENGLISH)
                             .format(new Date()));
// JDK 8:  2:27:03 PM PDT
// JDK 9:  2:27:03 PM Pacific Daylight Time

System.out.println(NumberFormat.getInstance(Locale.ENGLISH).format(Double.NaN));
// JDK 8:  �
// JDK 9:  NaN

System.out.println(new SimpleDateFormat("dd MMM", Locale.GERMANY)
                       .format(new GregorianCalendar(2024, Calendar.MARCH, 19)
                       .getTime()));
// JDK 8:  19 Mär
// JDK 9:  19 März

System.out.println(NumberFormat.getCurrencyInstance(Locale.ITALY).format(100));
// JDK 8:  € 100,00
// JDK 9:  100,00 €

System.out.println(new Locale("lt").getDisplayName(Locale.FRENCH));
// JDK 8:  lithuanien
// JDK 9:  lituanien

Прежде чем развёртывать приложения на JDK 9 или более поздней версии, где данные локалей CLDR используются по умолчанию, мы настоятельно рекомендуем проверить их на проблемы совместимости, запустив на JDK 8 с выбранным поставщиком CLDR. Для этого запустите среду выполнения Java 8 с параметром

$ java -Djava.locale.providers=CLDR,JRE ...

чтобы данные локалей CLDR имели приоритет над унаследованными данными локалей.

Если ваш код использует API, зависящие от локали, мы настоятельно рекомендуем как можно скорее доработать его, где это необходимо, чтобы он соответствовал данным локалей CLDR. Код, взаимодействующий с API, зависящими от локали, должен корректно работать, когда даты, время, валюты, языки, страны и часовые пояса форматируются и разбираются с использованием данных локалей CLDR.

Влияние на код может зависеть от того, передаются ли строковые представления дат, времени и т. п. во внешние по отношению к приложению системы или хранятся в них. Например, пусть у приложения есть объект Date, который нужно сохранить, поэтому оно форматирует Date для локали UK и сохраняет полученную строку в базе данных. Если позже, в том же сеансе, приложение извлечёт строку из базы данных и разберёт её как Date в локали UK, переход на данные локалей CLDR на это не повлияет. Приложение получит тот же Date, с которого начинало, поскольку и форматирование, и разбор выполняются на одном и том же JDK с одними и теми же данными локалей.

Однако предположим, что при сохранении строки в базе данных приложение работало на JDK 8, а при её извлечении работает на JDK 17. Объект Date был отформатирован в строку с использованием унаследованных данных локалей, а разбираться как Date строка будет с использованием данных локалей CLDR. Код вызовет java.text.ParseException, потому что, например, строка с дефисами "15-Mar-2024" не соответствует шаблону dd MMM yyyy, который используется для дат UK в CLDR. Из-за исключения приложение может завершиться с ошибкой или повести себя непредсказуемо.

Переход на данные локалей CLDR может затронуть не только код самого приложения, но и код, с помощью которого приложение тестируется. Модульные тесты часто содержат жёстко заданные строки с датой и временем, которые приложение должно разбирать с учётом локали. Если тесты были написаны для JDK 8, а приложение переносится на JDK 9 или более поздний выпуск, тесты могут не пройти.

Дальнейшее использование унаследованных данных локалей

Если дорабатывать код для форматирования и разбора строк с использованием данных локалей CLDR нецелесообразно, продолжить форматировать и разбирать строки с использованием унаследованных данных локалей можно тремя способами:

  1. Принудительно заставить API, зависящие от локали, использовать унаследованные данные локалей при запуске. Для этого запустите среду выполнения Java с параметром

    $ java -Djava.locale.providers=JRE,CLDR ...

    Значение системного свойства COMPAT можно использовать как синоним JRE, например -Djava.locale.providers=COMPAT,CLDR ...

    Принудительное использование унаследованных данных локалей следует рассматривать как временную меру. В одном из выпусков после JDK 9 будут доступны только данные локалей CLDR.

  2. Изменить код так, чтобы он всегда форматировал и разбирал строки по тем же шаблонам, что и в унаследованных данных локалей.

    Например, пусть ваш код использует зависящий от локали API SimpleDateFormat для форматирования объектов Date. На JDK 8 код мог получать SimpleDateFormat следующим образом:

    SimpleDateFormat fmt
        = (SimpleDateFormat)DateFormat.getDateInstance(DateFormat.MEDIUM, Locale.UK);
    // prints "19-Mar-2024" on JDK 8 but "19 Mar 2024" on JDK 9
    System.out.println(fmt.format(new Date()));

    Можно изменить код так, чтобы он создавал SimpleDateFormat напрямую, передавая нужный шаблон (компоненты даты, разделённые дефисами) в конструктор SimpleDateFormat:

    SimpleDateFormat fmt = new SimpleDateFormat("dd-MMM-yyyy", Locale.UK);
    // prints "19-Mar-2024", even on JDK 9
    System.out.println(fmt.format(new Date()));

    Это решение хорошо подходит для небольших приложений, а также для крупных приложений, которые хранят форматы в переменных-синглтонах, использование которых строго соблюдается во всей кодовой базе.

  3. Создать собственный поставщик данных локалей и включить его в приложение. Такой поставщик может переопределить поставщик CLDR, чтобы API, зависящие от локали, при форматировании и разборе строк отдавали приоритет шаблонам, заданным собственным поставщиком.

    Например, вот собственный поставщик данных локалей, с помощью которого на JDK 9 можно вернуть шаблон с дефисами для дат UK из JDK 8:

    package com.example.localization;
    import java.text.*;
    import java.text.spi.*;
    import java.util.*;
    
    public class HyphenatedUKDates extends DateFormatProvider {
    
         @Override
         public Locale[] getAvailableLocales() {
             return new Locale[]{Locale.UK};
         }
    
         @Override
         public DateFormat getDateInstance(int style, Locale locale) {
             assert locale.equals(Locale.UK);
             switch (style) {
                 case DateFormat.FULL:
                     return new SimpleDateFormat("EEEE, d MMMM yyyy");
                 case DateFormat.LONG:
                     return new SimpleDateFormat("dd MMMM yyyy");
                 case DateFormat.MEDIUM:
                     return new SimpleDateFormat("dd-MMM-yyyy");
                 case DateFormat.SHORT:
                     return new SimpleDateFormat("dd/MM/yy");
                 default:
                     throw new IllegalArgumentException("style not supported");
             }
         }
    
         @Override
         public DateFormat getDateTimeInstance(int dateStyle, int timeStyle,
                                               Locale locale)
         {
             ...
         }
    
         @Override
         public DateFormat getTimeInstance(int style, Locale locale) {
             ...
         }
    
    }

Дальнейшие планы в отношении унаследованных данных локалей

В одном из выпусков после JDK 9 мы полностью прекратим поставлять унаследованные данные локалей. Поддержка унаследованных данных локалей будет сокращаться постепенно:

Риски и допущения

  • Риск перехода с унаследованных данных локалей на данные локалей CLDR состоит в том, что некоторые приложения перестанут работать из-за изменившегося поведения API, зависящих от локали. Сбои могут возникать из-за того, что API возвращают неожиданные значения или выбрасывают исключения, к обработке которых приложения не готовы. Мы предполагаем, что в целом доля затронутых сбоями приложений будет небольшой.

  • Мы исходим из того, что переход на данные локалей CLDR — непрерывный процесс, в котором каждый следующий выпуск JDK переходит на последнюю версию CLDR, доступную от Unicode Consortium.

    Риск такого следования за CLDR состоит в том, что данные локалей CLDR со временем могут меняться несовместимым образом. Как правило, этот риск перевешивается преимуществами самых актуальных данных локалей, которые неизбежно меняются по мере того, как культуры развивают свои нормы и соглашения. Кроме того, этот риск перевешивается преимуществами использования в точности тех же данных локалей, что и на других платформах. Поэтому JDK будет включать данные локалей CLDR от Unicode Consortium как есть; мы не будем изменять их, если не возникнут исключительные обстоятельства.

    Дополнение, октябрь 2020 г.: пример такого риска — сокращённое название сентября в локали UK изменилось с Sep на Sept в CLDR версии 38, которая вошла в JDK 16.

  • Мы считаем нежелательным закреплять в стандарте использование CLDR в Java Platform. Мы не предлагаем делать обязательным ни использование CLDR в целом, ни использование конкретной версии CLDR в конкретном выпуске Java Platform.

    Интернационализация опирается на стандарты официальных и полуофициальных организаций, такие как языковые теги BCP 47 от IETF, база данных часовых поясов TZ от IANA и данные локалей CLDR от Unicode Consortium. Когда дело доходит до включения этих стандартов в Java Platform, приходится выбирать между предсказуемостью (реализации новой версии Platform обязаны использовать заданную версию стандарта) и гибкостью (реализации старой версии Platform можно обновить до новой версии стандарта без изменения Platform Specification).

    Исходя из многолетнего опыта следования этим стандартам, мы ставим гибкость выше предсказуемости. Например, данные IANA о часовых поясах меняются до нескольких раз в год, и крайне важно как можно быстрее переносить новые версии данных в старые выпуски. Поэтому Java Platform допускает, но не требует использования данных IANA о часовых поясах; если бы это было обязательным, для обновления старых выпусков потребовались бы трудоёмкие и дорогостоящие JCP Maintenance Releases, чтобы включить новые версии данных в Platform Specifications. Исходя из опыта следования CLDR, мы считаем уместным так же относиться к использованию данных локалей CLDR: CLDR — канонический выбор, но не обязательный. (К сожалению, использование данных локалей CLDR было по ошибке указано как стандартная возможность в Java SE 9 Platform Specification.)

    В этом отличие от набора символов Unicode, использование которого обязательно, поскольку он касается фундаментального вопроса в каждой программе на Java и поскольку задним числом вносимые изменения, требующие Maintenance Releases, случаются сравнительно редко.