НейроАгент

GraalVM Native Image: Включение полной локали данных для дат

Узнайте, как настроить GraalVM Native Image для включения полной локали данных для корректного форматирования дат/времени на арабском языке. Исправьте проблемы с резервным копированием данных CLDR и комплексной конфигурацией локали.

Вопрос

GraalVM Native Image: Как включить полные данные локали для форматирования даты/времени?

Я столкнулся с проблемой в GraalVM native-image, где локализованное форматирование даты/времени работает некорректно для определенных локалей. Конкретно, при форматировании дат на арабском языке вывод возвращается к английским сокращениям вместо отображения полного арабского текста.

Описание проблемы

При запуске того же Java-кода в режиме JVM я получаю ожидаемый вывод:

arabicGregorianDate = السبت 18\10\2025 م
englishGregorianDate = Saturday 18/10/2025 AD

Однако при компиляции в GraalVM native-image и запуске вывод получается таким:

arabicGregorianDate = Sat 18\10\2025 CE
englishGregorianDate = Saturday 18/10/2025 AD

Обратите внимание, что:

  • Арабское слово для субботы (السبت) заменяется на “Sat”
  • Арабский маркер эры (م) заменяется на “CE”

Пример кода

java
final var ARABIC_GREGORIAN_DATE_FORMATTER = DateTimeFormatter.ofPattern("EEEE dd\\MM\\yyyy G").localizedBy(Locale.forLanguageTag("ar"));
final var ENGLISH_GREGORIAN_DATE_FORMATTER = DateTimeFormatter.ofPattern("EEEE dd/MM/yyyy G").localizedBy(Locale.ENGLISH);

var now = Instant.now();
String arabicGregorianDate = ARABIC_GREGORIAN_DATE_FORMATTER.format(now.atZone(ZoneId.systemDefault()));
String englishGregorianDate = ENGLISH_GREGORIAN_DATE_FORMATTER.format(now.atZone(ZoneId.systemDefault()));

Log.info("arabicGregorianDate = " + arabicGregorianDate);
Log.info("englishGregorianDate = " + englishGregorianDate);

Текущая конфигурация

Я уже использую следующую конфигурацию:

  • -H:IncludeLocales=ar,en

Я также пробовал использовать агент трассировки GraalVM, но это не решило проблему.

Вопрос

Как настроить GraalVM native-image на включение необходимых данных локали (возможно, данных CLDR), чтобы форматирование даты на арабском языке работало корректно?

Пример воспроизведения доступен по адресу: https://github.com/Eng-Fouad/graal-native-date-locale

НейроАгент

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


Содержание


Понимание ограничений локали в GraalVM

GraalVM Native Image работает иначе, чем традиционное выполнение JVM, в отношении обработки ресурсов. По умолчанию он включает только минимальный набор данных локали, необходимый для работы приложения, что часто приводит к неполной поддержке локали.

Согласно GitHub issue о поддержке локали в Graal, начиная с GraalVM 20.2.0, нативные исполняемые файлы содержат только одну локаль, которая была возвращена Locale.getDefault() в сборочной JVM. Это ограничение влияет на форматирование даты/времени, форматирование чисел и другие операции, чувствительные к локали.

Суть проблемы заключается в том, как GraalVM обрабатывает ресурсы локали:

  • Native Image выполняет статический анализ во время сборки
  • Он включает только ресурсы, которые статически достижимы
  • Данные локали считаются ресурсом, который должен быть явно включен
  • Данные Common Locale Data Repository (CLDR) не включаются автоматически

Ключевое понимание: Данные CLDR, поддерживаемые Консорциумом Unicode, обеспечивают более высокое качество данных локали, чем устаревшие данные в JDK 8, как упоминается в JEP 252: Use CLDR Locale Data by Default.

Параметры конфигурации для данных локали

GraalVM предоставляет несколько параметров конфигурации для включения данных локали в нативные образы:

1. Включение конкретных локалей

Параметр -H:IncludeLocales позволяет указать, какие локали включить. Однако, как вы обнаружили, этого может быть недостаточно для форматирования даты:

bash
-H:IncludeLocales=ar,en

Формат тегов локали следует стандарту IETF BCP 47. Для арабского языка можно использовать:

  • ar - общий арабский
  • ar-SA - арабский для Саудовской Аравии
  • ar-EG - арабский для Египта

2. Включение всех локалей

Для всесторонней поддержки локали можно использовать:

bash
-H:+IncludeAllLocales

Как указано в документации GraalVM по ресурсам, этот параметр включает все локали, но значительно увеличивает размер результирующего исполняемого файла.

3. Конфигурация ресурсов

Также можно настроить ресурсы через файл конфигурации JSON:

json
{
  "resources": {
    "includes": [
      "META-INF/**"
    ],
    "bundles": [
      "java.text.resources.*"
    ]
  }
}

Пошаговые решения

Решение 1: Расширенная конфигурация локали

Попробуйте более комплексную спецификацию локали:

bash
-H:IncludeLocales=ar,en,ar-SA,ar-EG

Это гарантирует, что различные варианты арабской локали будут включены.

Решение 2: Правильное использование трассирующего агента

Трассирующий агент нужно запускать с фактическим кодом приложения, который использует форматирование даты локали. Вот правильный подход:

  1. Запустите приложение с трассирующим агентом:
bash
java -agentlib:native-image-agent=config-output-dir=src/main/resources/META-INF/native-image \
     -jar your-application.jar
  1. Убедитесь, что вы фактически вызываете форматирование даты на арабском языке во время этого запуска

  2. Пересоберите нативный образ с сгенерированной конфигурацией:

bash
native-image --config-dir=src/main/resources/META-INF/native-image -H:IncludeLocales=ar,en \
             -jar your-application.jar

Решение 3: Конфигурация набора ресурсов

Создайте файл конфигурации ресурсов (src/main/resources/META-INF/native-image/resource-config.json):

json
{
  "resources": {
    "includes": [
      "**/*.json",
      "**/*.properties"
    ],
    "bundles": [
      "java.text.resources.DateFormatData",
      "java.text.resources.DateFormatData_en",
      "java.text.resources.DateFormatData_ar"
    ]
  }
}

Затем соберите с помощью:

bash
native-image -H:ConfigurationFileDirectories=src/main/resources/META-INF/native-image \
             -H:IncludeLocales=ar,en -jar your-application.jar

Решение 4: Использование конфигурации времени сборки

Для приложений Quarkus можно использовать подход @AutomaticFeature, упомянутый в обсуждении на Stack Overflow:

java
@BuildStep
@Record(ExecutionTime.STATIC_INIT)
void configureNativeImage(NativeImageBuildItem nativeImageBuildItem) {
    nativeImageBuildItem.getBuildArgs()
        .add("--enable-url-protocols=http,https")
        .add("-H:IncludeLocales=ar,en");
}

Альтернативные подходы

1. Реализация пользовательских данных локали

Если встроенные данные локали недостаточны, вы можете реализовать пользовательское решение:

java
public class ArabicDateFormatter {
    private static final Map<String, String> ARABIC_DAY_NAMES = Map.of(
        "Saturday", "السبت",
        "Sunday", "الأحد",
        // ... другие дни
    );
    
    private static final Map<String, String> ARABIC_ERA_NAMES = Map.of(
        "AD", "م",
        "CE", "م"
    );
    
    public static String formatArabicDate(Instant instant) {
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("EEEE dd\\MM\\yyyy G")
            .withLocale(Locale.ENGLISH);
        
        String formatted = formatter.format(instant.atZone(ZoneId.systemDefault()));
        
        // Замена английских имен на арабские
        for (Map.Entry<String, String> entry : ARABIC_DAY_NAMES.entrySet()) {
            formatted = formatted.replace(entry.getKey(), entry.getValue());
        }
        
        for (Map.Entry<String, String> entry : ARABIC_ERA_NAMES.entrySet()) {
            formatted = formatted.replace(entry.getKey(), entry.getValue());
        }
        
        return formatted;
    }
}

2. Внешние ресурсы локали

Упакуйте требуемые ресурсы локали как внешние файлы и загружайте их во время выполнения:

java
// Загрузка ресурсов локали из внешнего файла
private static void loadLocaleResources() {
    try (InputStream is = ArabicLocaleSupport.class.getResourceAsStream("/locales/arabic-date-format.properties")) {
        if (is != null) {
            // Загрузка и настройка данных локали
        }
    } catch (IOException e) {
        throw new RuntimeException("Не удалось загрузить ресурсы арабской локали", e);
    }
}

Лучшие практики и соображения

1. Стратегия тестирования локали

Всегда тестируйте ваше приложение с целевыми локалями перед развертыванием:

java
public class LocaleTest {
    @Test
    void testArabicDateFormatting() {
        Locale arabicLocale = Locale.forLanguageTag("ar-SA");
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("EEEE dd\\MM\\yyyy G")
            .localizedBy(arabicLocale);
        
        String result = formatter.format(Instant.now().atZone(ZoneId.systemDefault()));
        
        assertTrue(result.contains("السبت"), "Должен содержать арабскую субботу");
        assertTrue(result.contains("م"), "Должен содержать маркер эры на арабском");
    }
}

2. Оптимизация конфигурации сборки

Сбалансируйте между поддержкой локали и размером исполняемого файла:

bash
# Для производственных сборок с конкретными локалями
native-image -H:IncludeLocales=ar,en,ar-SA,ar-EG,fr,de \
             --no-fallback -jar your-application.jar

# Для разработки со всеми локалями
native-image -H:+IncludeAllLocales \
             -jar your-application-dev.jar

3. Обнаружение локали во время выполнения

Реализуйте обнаружение локали во время выполнения как резервный вариант:

java
public static DateTimeFormatter createLocalizedFormatter(String languageTag) {
    try {
        Locale locale = Locale.forLanguageTag(languageTag);
        return DateTimeFormatter.ofPattern("EEEE dd\\MM\\yyyy G").localizedBy(locale);
    } catch (Exception e) {
        // Откат к английскому
        return DateTimeFormatter.ofPattern("EEEE dd\\MM\\yyyy G").localizedBy(Locale.ENGLISH);
    }
}

Отладка и проверка

1. Проверка включения локали

Проверьте, какие локали фактически включены в вашем нативном образе:

java
import java.text.spi.DateFormatProvider;
import java.util.Locale;

public class LocaleDebugger {
    public static void debugLocales() {
        Locale[] availableLocales = Locale.getAvailableLocales();
        System.out.println("Доступные локали: " + availableLocales.length);
        
        for (Locale locale : availableLocales) {
            if (locale.getLanguage().equals("ar")) {
                System.out.println("Найдена арабская локаль: " + locale);
            }
        }
        
        // Тестирование форматирования даты
        testDateFormatting(Locale.forLanguageTag("ar"));
    }
    
    private static void testDateFormatting(Locale locale) {
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("EEEE dd\\MM\\yyyy G")
            .localizedBy(locale);
        System.out.println("Отформатированная дата в " + locale + ": " + 
            formatter.format(Instant.now().atZone(ZoneId.systemDefault())));
    }
}

2. Анализ нативного образа

Используйте инструменты GraalVM для анализа вашего нативного образа:

bash
# Анализ ресурсов нативного образа
native-image -H:+PrintAnalysisResources -jar your-application.jar

# Проверка отсутствующих ресурсов
native-image -H:+ReportExceptionStackTraces -jar your-application.jar

3. Воспроизведение проблемы

На основе вашего репозитория GitHub, вот минимальная настройка для воспроизведения:

java
public class ArabicDateReproducer {
    public static void main(String[] args) {
        final var ARABIC_GREGORIAN_DATE_FORMATTER = DateTimeFormatter.ofPattern("EEEE dd\\MM\\yyyy G")
            .localizedBy(Locale.forLanguageTag("ar"));
        final var ENGLISH_GREGORIAN_DATE_FORMATTER = DateTimeFormatter.ofPattern("EEEE dd/MM/yyyy G")
            .localizedBy(Locale.ENGLISH);

        var now = Instant.now();
        String arabicGregorianDate = ARABIC_GREGORIAN_DATE_FORMATTER.format(now.atZone(ZoneId.systemDefault()));
        String englishGregorianDate = ENGLISH_GREGORIAN_DATE_FORMATTER.format(now.atZone(ZoneId.systemDefault()));

        System.out.println("arabicGregorianDate = " + arabicGregorianDate);
        System.out.println("englishGregorianDate = " + englishGregorianDate);
    }
}

Соберите и протестируйте с помощью:

bash
# Режим JVM (должен работать правильно)
java ArabicDateReproducer.java

# Нативный режим (может быть проблематичным)
native-image ArabicDateReproducer
./ArabicDateReproducer

Заключение

  1. Основная проблема: GraalVM Native Image не автоматически включает полные данные локали, что приводит к тому, что форматирование дат на арабском языке возвращается к английским сокращениям.

  2. Основные решения: Используйте -H:IncludeLocales=ar,en с полными тегами локали или -H:+IncludeAllLocales для полной поддержки локали.

  3. Расширенная конфигурация: Реализуйте файлы конфигурации ресурсов и правильное использование трассирующего агента для обеспечения включения всех требуемых ресурсов локали.

  4. Стратегия тестирования: Всегда тестируйте с целевыми локалями перед развертыванием и реализуйте механизмы резервного варианта для крайних случаев.

  5. Компромисс производительности: Включение всех локалей увеличивает размер исполняемого файла, поэтому балансируйте между функциональностью и использованием ресурсов в зависимости от требований вашего приложения.

Для наиболее надежного решения объедините несколько подходов: укажите точные требования к локали, используйте трассирующий агент для перехвата отсутствующих ресурсов и реализуйте пользовательские механизмы резервного варианта для производственных сред.

Источники

  1. Документация GraalVM Native Image Resources
  2. GitHub Issue: Support for non-default locale
  3. JEP 252: Use CLDR Locale Data by Default
  4. Stack Overflow: Quarkus GraalVM native image DateTimeFormatter and Localization
  5. Stack Overflow: GraalVM native-image Java i18n Locale problem
  6. GraalVM Native Image Build Configuration