diff --git a/pyproject.toml b/pyproject.toml index 2984326b..6e7a727c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ keywords = ["jdk", "java", "jvm", "jre"] name = "jdk4py" readme = "README.md" requires-python = ">=3.10" -version = "25.0.2.0" +version = "25.0.2.1" [project.urls] Repository = "https://github.com/activeviam/jdk4py" @@ -47,6 +47,7 @@ ignore = [ "D203", "D211", "D212", + "E501", "EM102", "ISC001", "S101", diff --git a/src/jdk4py/_included_locales.py b/src/jdk4py/_included_locales.py index 97ab8d58..f1aa0aae 100644 --- a/src/jdk4py/_included_locales.py +++ b/src/jdk4py/_included_locales.py @@ -1,17 +1,29 @@ +from collections.abc import Collection +from typing import Literal, TypeAlias, get_args + +_RegionSpecificLocale: TypeAlias = Literal[ + "bn-IN", + "da-DK", + "de-DE", + "en-GB", + "en-US", + "es-ES", + "es-MX", + "fr-FR", + "it-IT", + "ja-JP", + "pt-BR", + "ru-RU", + "zh-CN", +] +_REGION_SPECIFIC_LOCALES: Collection[str] = get_args(_RegionSpecificLocale) + INCLUDED_LOCALES = frozenset( - [ - "bn-IN", - "da-DK", - "de-DE", - "en-US", - "en-GB", - "es-ES", - "es-MX", - "fr-FR", - "it-IT", - "ja-JP", - "pt-BR", - "ru-RU", - "zh-CN", - ], + { + locale + for tag in _REGION_SPECIFIC_LOCALES + # Expand each region-specific locale to also include its language-only locale so that `jlink --include-locales` retains the language-only locale bundles (e.g. `FormatData_fr.class`). + # See https://github.com/openjdk/jdk/blob/jdk-25%2B35/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/plugins/IncludeLocalesPlugin.java. + for locale in (tag, tag.split("-", maxsplit=1)[0]) + }, ) diff --git a/tests/resources/PrintLocaleNumberFormats.jar b/tests/resources/PrintLocaleNumberFormats.jar new file mode 100644 index 00000000..29076567 Binary files /dev/null and b/tests/resources/PrintLocaleNumberFormats.jar differ diff --git a/tests/resources/PrintLocaleNumberFormats.java b/tests/resources/PrintLocaleNumberFormats.java new file mode 100644 index 00000000..8cebb931 --- /dev/null +++ b/tests/resources/PrintLocaleNumberFormats.java @@ -0,0 +1,19 @@ +package resources; + +import java.text.DecimalFormatSymbols; +import java.util.Locale; + +public class PrintLocaleNumberFormats { + + public static void main(String[] args) { + for (final Locale locale: DecimalFormatSymbols.getAvailableLocales()) { + final DecimalFormatSymbols dfs = DecimalFormatSymbols.getInstance(locale); + System.out.println( + locale.toLanguageTag() + + "\t" + ((int) dfs.getDecimalSeparator()) + + "\t" + ((int) dfs.getGroupingSeparator()) + ); + } + } + +} diff --git a/tests/test_jdk4py.py b/tests/test_jdk4py.py index a89f77bf..4bd5e2a3 100644 --- a/tests/test_jdk4py.py +++ b/tests/test_jdk4py.py @@ -1,9 +1,11 @@ import re +from dataclasses import dataclass from pathlib import Path from subprocess import run +from typing import get_args from jdk4py import JAVA, JAVA_VERSION -from jdk4py._included_locales import INCLUDED_LOCALES +from jdk4py._included_locales import INCLUDED_LOCALES, _RegionSpecificLocale _TEST_RESOURCES_DIRECTORY = Path(__file__).parent / "resources" @@ -43,4 +45,75 @@ def test_included_locales() -> None: locale.replace("_", "-") for locale in completed_process.stdout.strip().splitlines() } - assert locales.issuperset(INCLUDED_LOCALES) + assert locales >= INCLUDED_LOCALES + + +@dataclass(frozen=True) +class _NumberFormattingSeparators: + decimal_separator: str + grouping_separator: str + + +_COMMA = "," +_DOT = "." +_NARROW_NO_BREAK_SPACE = chr(0x202F) +_NO_BREAK_SPACE = chr(0x00A0) + + +def _get_number_formatting_separators( + locale: _RegionSpecificLocale, / +) -> _NumberFormattingSeparators: + match locale: + case "bn-IN" | "en-GB" | "en-US" | "es-MX" | "ja-JP" | "zh-CN": + return _NumberFormattingSeparators( + decimal_separator=_DOT, grouping_separator=_COMMA + ) + case "da-DK" | "de-DE" | "es-ES" | "it-IT" | "pt-BR": + return _NumberFormattingSeparators( + decimal_separator=_COMMA, grouping_separator=_DOT + ) + case "fr-FR": + return _NumberFormattingSeparators( + decimal_separator=_COMMA, + grouping_separator=_NARROW_NO_BREAK_SPACE, + ) + case "ru-RU": + return _NumberFormattingSeparators( + decimal_separator=_COMMA, grouping_separator=_NO_BREAK_SPACE + ) + + +def test_locale_data_inclusion() -> None: + """ + Check that each included locale uses its expected number formatting separators. + + ``jlink --include-locales=fr-FR`` does not retain ``FormatData_fr.class``, + so ``DecimalFormatSymbols.getInstance(Locale.forLanguageTag("fr-FR"))`` + silently returns ROOT symbols. + + ``INCLUDED_LOCALES`` must therefore list each region-specific locale alongside its language-only parent. + """ + expected = { + locale: _get_number_formatting_separators(locale) + for locale in get_args(_RegionSpecificLocale) + } + + completed_process = run( # noqa: S603 + [JAVA, "-jar", _TEST_RESOURCES_DIRECTORY / "PrintLocaleNumberFormats.jar"], + capture_output=True, + check=True, + text=True, + ) + actual = { + locale: _NumberFormattingSeparators( + decimal_separator=chr(int(decimal_cp)), + grouping_separator=chr(int(grouping_cp)), + ) + for line in completed_process.stdout.strip().splitlines() + for locale, decimal_cp, grouping_cp in [line.split("\t")] + if locale in expected + } + + assert actual == expected, ( + "CLDR data not loaded for some locales (likely a missing language-only bundle)" + ) diff --git a/uv.lock b/uv.lock index 2f3d3793..7f6a0583 100644 --- a/uv.lock +++ b/uv.lock @@ -34,7 +34,7 @@ wheels = [ [[package]] name = "jdk4py" -version = "21.0.8.2" +version = "25.0.2.1" source = { editable = "." } [package.dev-dependencies]