Skip to content

Commit

Permalink
feat: add support for the supportedLocalesOf() static methods (#16)
Browse files Browse the repository at this point in the history
  • Loading branch information
ramsey authored Sep 11, 2023
1 parent 9522679 commit eff8e9b
Show file tree
Hide file tree
Showing 47 changed files with 1,800 additions and 129 deletions.
1 change: 1 addition & 0 deletions .clang-tidy
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Checks: '
-hicpp-*,
-llvm-else-after-return,
-llvm-header-guard,
-llvm-include-order,
-llvmlibc-*,
-misc-unused-parameters,
-modernize-macro-to-enum,
Expand Down
3 changes: 3 additions & 0 deletions config.m4
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ if test "$PHP_ECMA_INTL" != "no"; then
src/ecma402/numbering_system.c \
src/ecma402/time_zone.c \
src/php/classes/category.c \
src/php/classes/collator.c \
src/php/classes/intl.c \
src/php/classes/locale.c \
src/php/classes/locale_character_direction.c \
Expand All @@ -50,7 +51,9 @@ if test "$PHP_ECMA_INTL" != "no"; then
src/php/classes/locale_week_day.c \
src/php/classes/locale_week_info.c \
src/php/classes/options.c \
src/php/classes/supported_locales_options.c \
src/php/ecma_intl.c \
src/php/supported_locales.c \
"

PHP_ECMA_INTL_CXX_SOURCES=" \
Expand Down
2 changes: 2 additions & 0 deletions docs/readthedocs/reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ Reference

reference/ecma-intl
reference/ecma-intl-category
reference/ecma-intl-collator
reference/ecma-intl-locale
reference/ecma-intl-locale-characterdirection
reference/ecma-intl-locale-options
reference/ecma-intl-locale-textinfo
reference/ecma-intl-locale-weekday
reference/ecma-intl-locale-weekinfo
reference/ecma-intl-supportedlocales-options
18 changes: 18 additions & 0 deletions docs/readthedocs/reference/ecma-intl-collator.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
.. _ecma.intl.collator:

====================
Ecma\\Intl\\Collator
====================

.. php:namespace:: Ecma\Intl
.. php:class:: Collator
.. php:staticmethod:: supportedLocalesOf($locales[, $options = null]): string[]
Returns an array of those locales provided that are supported by this
implementation without having to fall back to the default locale.

:param iterable<Stringable|string> | Stringable | string | null $locales: One
or more language tags to check for support.
:param Ecma\\Intl\\SupportedLocales\\Options $options: Options to use when
evaluating supported locales.
47 changes: 47 additions & 0 deletions docs/readthedocs/reference/ecma-intl-supportedlocales-options.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
.. _ecma.intl.supportedlocales.options:

=====================================
Ecma\\Intl\\SupportedLocales\\Options
=====================================

.. php:namespace:: Ecma\Intl\SupportedLocales
.. php:class:: Options
Options to use when evaluating supported locales.

ECMA-402 defines a ``supportedLocalesOf()`` static method that is available
on many of the classes provided in the specification. For example, see
`Intl.Collator.supportedLocalesOf <https://tc39.es/ecma402/#sec-intl.collator.supportedlocalesof>`_.
Each of these methods accepts an ``options`` object that specifies the
locale matcher algorithm to use when evaluating supported locales. In this
PHP implementation, :php:class:`Ecma\\Intl\\SupportedLocales\\Options` provides
a type for this options object.

.. php:attr:: localeMatcher: string | null, readonly
The locale-matching algorithm to use when evaluating supported locales.

.. php:method:: __construct([$localeMatcher = null])
:param Stringable | string | null $localeMatcher: The locale-matching
algorithm to use when evaluating supported locales. This may be one
of the values ``"best fit"`` or ``"lookup"``. Other implementations
may define additional values.

.. php:method:: jsonSerialize(): object
Returns an object of these options, suitable for serializing to JSON.

Please note that any options with a ``null`` value will not be included
in the object this method returns. This allows the JSON to be passed to
JavaScript contexts, where these properties are considered ``undefined``.

.. php:method:: current(): string | bool
.. php:method:: next(): void
.. php:method:: key(): string
.. php:method:: valid(): bool
.. php:method:: rewind(): void
33 changes: 33 additions & 0 deletions package.xml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ Add Locale::$currency and Locale\Options::$currency properties. ECMA-402 does no
<file name="category.h" role="src"/>
<file name="category.stub.php" role="src"/>
<file name="category_arginfo.h" role="src"/>
<file name="collator.c" role="src"/>
<file name="collator.h" role="src"/>
<file name="collator.stub.php" role="src"/>
<file name="collator_arginfo.h" role="src"/>
<file name="intl.c" role="src"/>
<file name="intl.h" role="src"/>
<file name="intl.stub.php" role="src"/>
Expand Down Expand Up @@ -109,10 +113,16 @@ Add Locale::$currency and Locale\Options::$currency properties. ECMA-402 does no
<file name="locale_week_info_arginfo.h" role="src"/>
<file name="options.c" role="src"/>
<file name="options.h" role="src"/>
<file name="supported_locales_options.c" role="src"/>
<file name="supported_locales_options.h" role="src"/>
<file name="supported_locales_options.stub.php" role="src"/>
<file name="supported_locales_options_arginfo.h" role="src"/>
</dir>
<file name="ecma_intl.c" role="src"/>
<file name="ecma_intl.h" role="src"/>
<file name="php_common.h" role="src"/>
<file name="supported_locales.c" role="src"/>
<file name="supported_locales.h" role="src"/>
</dir>
</dir>
<dir name="tests">
Expand Down Expand Up @@ -153,6 +163,17 @@ Add Locale::$currency and Locale\Options::$currency properties. ECMA-402 does no
<dir name="phpt">
<dir name="Intl">
<file name="Category-001.phpt" role="test"/>
<file name="Collator_supportedLocalesOf-001.phpt" role="test"/>
<file name="Collator_supportedLocalesOf-002.phpt" role="test"/>
<file name="Collator_supportedLocalesOf-003.phpt" role="test"/>
<file name="Collator_supportedLocalesOf-004.phpt" role="test"/>
<file name="Collator_supportedLocalesOf-005.phpt" role="test"/>
<file name="Collator_supportedLocalesOf-006.phpt" role="test"/>
<file name="Collator_supportedLocalesOf-007.phpt" role="test"/>
<file name="Collator_supportedLocalesOf-008.phpt" role="test"/>
<file name="Collator_supportedLocalesOf-009.phpt" role="test"/>
<file name="Collator_supportedLocalesOf-010.phpt" role="test"/>
<file name="Collator_supportedLocalesOf-011.phpt" role="test"/>
<dir name="Locale">
<file name="CharacterDirection-001.phpt" role="test"/>
<file name="Options-001.phpt" role="test"/>
Expand Down Expand Up @@ -195,6 +216,18 @@ Add Locale::$currency and Locale\Options::$currency properties. ECMA-402 does no
<file name="Locale-getTimeZones-001.phpt" role="test"/>
<file name="Locale-getWeekInfo-001.phpt" role="test"/>
<file name="Locale-toString-001.phpt" role="test"/>
<dir name="SupportedLocales">
<file name="Options-001.phpt" role="test"/>
<file name="Options-002.phpt" role="test"/>
<file name="Options-003.phpt" role="test"/>
<file name="Options-004.phpt" role="test"/>
<file name="Options-005.phpt" role="test"/>
<file name="Options-006.phpt" role="test"/>
<file name="Options-007.phpt" role="test"/>
<file name="Options-008.phpt" role="test"/>
<file name="Options-009.phpt" role="test"/>
<file name="Options-010.phpt" role="test"/>
</dir>
</dir>
<file name="Intl-001.phpt" role="test"/>
<file name="Intl_getCanonicalLocales-001.phpt" role="test"/>
Expand Down
173 changes: 127 additions & 46 deletions src/ecma402/locale.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
int const len_##property = ecma402_##method(id, property, locale->status, true); \
if (ecma402_hasError(locale->status)) { \
free(property); \
free(id); \
return locale; \
} \
if (len_##property >= 0) { \
Expand All @@ -61,6 +62,7 @@ namespace {
bool isCanonicalized);
int getNumberingSystemsForLocale(char *localeId, const char **values);
int getTimeZonesForLocale(char *localeId, const char **values);
int languageTagForLocaleId(const char *localeId, char *languageTag, ecma402_errorStatus *status);

} // namespace

Expand Down Expand Up @@ -143,6 +145,60 @@ ecma402_locale *ecma402_applyLocaleOptions(ecma402_locale *locale, const char *c
return ecma402_initLocale(builtLocale.c_str());
}

int ecma402_bestAvailableLocale(char **availableLocales, int availableLocalesCount, const char *localeId,
char *bestAvailable, bool isCanonicalized)
{
std::string candidate;
char *languageTag;

if (localeId == nullptr) {
return -1;
}

if (isCanonicalized) {
candidate = localeId;
} else {
ecma402_errorStatus *status = ecma402_initErrorStatus();
languageTag = (char *)malloc(sizeof(char) * ULOC_FULLNAME_CAPACITY);

languageTagForLocaleId(localeId, languageTag, status);

if (ecma402_hasError(status)) {
free(languageTag);
ecma402_freeErrorStatus(status);
return -1;
}

candidate = languageTag;
free(languageTag);
ecma402_freeErrorStatus(status);
}

while (!candidate.empty()) {
char **item = availableLocales;
for (int i = 0; i < availableLocalesCount; i++) {
if (candidate == *item) {
strcpy(bestAvailable, *item);
return strlen(bestAvailable);
}
++item;
}

size_t hyphenPos = candidate.rfind('-');
if (hyphenPos == std::string::npos) {
return -1;
}

if (hyphenPos >= 2 && candidate[hyphenPos - 2] == '-') {
hyphenPos -= 2;
}

candidate = candidate.substr(0, hyphenPos);
}

return -1;
}

int ecma402_canonicalizeLocaleList(const char **locales, int localesLength, char **canonicalized,
ecma402_errorStatus *status)
{
Expand Down Expand Up @@ -173,11 +229,6 @@ int ecma402_canonicalizeLocaleList(const char **locales, int localesLength, char

int ecma402_canonicalizeUnicodeLocaleId(const char *localeId, char *canonicalized, ecma402_errorStatus *status)
{
icu::Locale canonicalLocale;
UErrorCode icuStatus = U_ZERO_ERROR;
UBool const strict = 1;
char *unicodeLocaleId;

if (localeId == nullptr) {
return -1;
}
Expand All @@ -187,47 +238,7 @@ int ecma402_canonicalizeUnicodeLocaleId(const char *localeId, char *canonicalize
return -1;
}

canonicalLocale = icu::Locale::createCanonical(localeId);
if (canonicalLocale == nullptr) {
ecma402_ecmaError(status, CANNOT_CREATE_LOCALE_ID, "Invalid language tag \"%s\"", localeId);
return -1;
}

std::string const locale = canonicalLocale.toLanguageTag<std::string>(icuStatus);
if (U_FAILURE(icuStatus) != U_ZERO_ERROR) {
ecma402_icuError(status, icuStatus, "Invalid language tag \"%s\"", localeId);
return -1;
}

// If the input localeId is not "und," but we got "und," then return 0.
if (strcasecmp(locale.c_str(), UNDETERMINED_LANGUAGE) == 0 && strcasecmp(localeId, UNDETERMINED_LANGUAGE) != 0) {
ecma402_ecmaError(status, UNDEFINED_LOCALE_ID, "Invalid language tag \"%s\"", localeId);
return -1;
}

// This additional conversion step forces tags like "en-latn-us-co-foo" and
// "de-de_euro" to result in failures, which is the desired result.
unicodeLocaleId = (char *)malloc(sizeof(char) * ULOC_FULLNAME_CAPACITY);
int const length = uloc_toLanguageTag(locale.c_str(), unicodeLocaleId, ULOC_FULLNAME_CAPACITY, strict, &icuStatus);

if (U_FAILURE(icuStatus) != U_ZERO_ERROR || strlen(unicodeLocaleId) == 0 || unicodeLocaleId == nullptr) {
if (U_FAILURE(icuStatus) != U_ZERO_ERROR) {
ecma402_icuError(status, icuStatus, "Invalid language tag \"%s\"", localeId);
} else {
ecma402_ecmaError(status, INVALID_LOCALE_ID, "Invalid language tag \"%s\"", localeId);
}

if (unicodeLocaleId != nullptr) {
free(unicodeLocaleId);
}

return -1;
}

memcpy(canonicalized, unicodeLocaleId, length + 1);
free(unicodeLocaleId);

return length;
return languageTagForLocaleId(localeId, canonicalized, status);
}

void ecma402_freeLocale(ecma402_locale *locale)
Expand Down Expand Up @@ -460,6 +471,24 @@ ecma402_locale *ecma402_initLocale(const char *localeId)
return locale;
}

int ecma402_intlAvailableLocales(char **locales)
{
ecma402_errorStatus *status = ecma402_initErrorStatus();
const int count = uloc_countAvailable();
int i, counted = 0;

for (i = 0; i < count; i++) {
char *locale = (char *)malloc(sizeof(char) * ULOC_FULLNAME_CAPACITY);
if (languageTagForLocaleId(uloc_getAvailable(i), locale, status) > 0) {
locales[counted] = strdup(locale);
counted++;
}
free(locale);
}

return counted;
}

bool ecma402_isNumeric(const char *localeId, ecma402_errorStatus *status, bool isCanonicalized)
{
char *numeric;
Expand Down Expand Up @@ -837,4 +866,56 @@ namespace {
return count;
}

int languageTagForLocaleId(const char *localeId, char *languageTag, ecma402_errorStatus *status)
{
icu::Locale canonicalLocale;
UErrorCode icuStatus = U_ZERO_ERROR;
UBool const strict = 1;
char *unicodeLocaleId;

canonicalLocale = icu::Locale::createCanonical(localeId);
if (canonicalLocale == nullptr) {
ecma402_ecmaError(status, CANNOT_CREATE_LOCALE_ID, "Invalid language tag \"%s\"", localeId);
return -1;
}

std::string const locale = canonicalLocale.toLanguageTag<std::string>(icuStatus);
if (U_FAILURE(icuStatus) != U_ZERO_ERROR) {
ecma402_icuError(status, icuStatus, "Invalid language tag \"%s\"", localeId);
return -1;
}

// If the input localeId is not "und," but we got "und," then return 0.
if (strcasecmp(locale.c_str(), UNDETERMINED_LANGUAGE) == 0 &&
strcasecmp(localeId, UNDETERMINED_LANGUAGE) != 0) {
ecma402_ecmaError(status, UNDEFINED_LOCALE_ID, "Invalid language tag \"%s\"", localeId);
return -1;
}

// This additional conversion step forces tags like "en-latn-us-co-foo" and
// "de-de_euro" to result in failures, which is the desired result.
unicodeLocaleId = (char *)malloc(sizeof(char) * ULOC_FULLNAME_CAPACITY);
int const length =
uloc_toLanguageTag(locale.c_str(), unicodeLocaleId, ULOC_FULLNAME_CAPACITY, strict, &icuStatus);

if (U_FAILURE(icuStatus) != U_ZERO_ERROR || strlen(unicodeLocaleId) == 0 || unicodeLocaleId == nullptr) {
if (U_FAILURE(icuStatus) != U_ZERO_ERROR) {
ecma402_icuError(status, icuStatus, "Invalid language tag \"%s\"", localeId);
} else {
ecma402_ecmaError(status, INVALID_LOCALE_ID, "Invalid language tag \"%s\"", localeId);
}

if (unicodeLocaleId != nullptr) {
free(unicodeLocaleId);
}

return -1;
}

memcpy(languageTag, unicodeLocaleId, length + 1);
free(unicodeLocaleId);

return length;
}

} // namespace
Loading

0 comments on commit eff8e9b

Please sign in to comment.