packages feed

fluent-icu-1.0.0: cbits.c

#include <string.h>

#include <unicode/stringoptions.h>
#include <unicode/ucal.h>
#include <unicode/ucasemap.h>
#include <unicode/udatpg.h>
#include <unicode/uformattednumber.h>
#include <unicode/uformattedvalue.h>
#include <unicode/uloc.h>
#include <unicode/unumberformatter.h>
#include <unicode/upluralrules.h>
#include <unicode/ustring.h>

/*
 * Retrieve the localised display name of a locale translated into the display locale.
 */
int32_t fluent_get_display_language(
    char const* locale, char const* displayLocale, char* name, int32_t capacity)
{
    if (name == NULL || capacity <= 0) return -1;

    UErrorCode status = U_ZERO_ERROR;
    UChar display[256];
    int32_t length = uloc_getDisplayLanguage(locale, displayLocale, display, 256, &status);
    if (U_FAILURE(status) || status == U_USING_DEFAULT_WARNING) return -1;

    int32_t written = 0;
    u_strToUTF8(name, capacity, &written, display, length, &status);
    if (U_FAILURE(status)) return -1;
    return written;
}

/*
 * Apply title-casing to a UTF-8 string according to the rules of the given locale.
 * Existing casing isn't overwritten where inappropriate.
 */
int32_t fluent_format_titlecase(
    char const* locale, char const* text, int32_t textLength, char* capitalised, int32_t capacity)
{
    if (capitalised == NULL || capacity <= 0) return -1;

    UErrorCode status = U_ZERO_ERROR;
    UCaseMap* caseMap =
        ucasemap_open(locale, U_TITLECASE_SENTENCES | U_TITLECASE_NO_LOWERCASE, &status);
    if (U_FAILURE(status)) {
        if (caseMap != NULL) ucasemap_close(caseMap);
        return -1;
    }

    int32_t written =
        ucasemap_utf8ToTitle(caseMap, capitalised, capacity, text, textLength, &status);
    ucasemap_close(caseMap);
    if (U_FAILURE(status)) return -1;
    return written;
}

/*
 * Format an exact decimal string according to an ICU number skeleton and locale.
 * The caller owns the returned object and must release it with unumf_closeResult().
 * Returns NULL on failure, with the reason written to *status.
 */
static UFormattedNumber* fluent_format_decimal_number(char const* locale, char const* skeleton,
    char const* decimal, int32_t decimalLength, UErrorCode* status)
{
    UChar wantedSkeleton[128];
    int32_t skeletonLength = 0;
    u_strFromUTF8(wantedSkeleton, 128, &skeletonLength, skeleton, -1, status);
    if (U_FAILURE(*status)) return NULL;

    UNumberFormatter* formatter =
        unumf_openForSkeletonAndLocale(wantedSkeleton, skeletonLength, locale, status);
    if (U_FAILURE(*status)) {
        if (formatter != NULL) unumf_close(formatter);
        return NULL;
    }

    UFormattedNumber* result = unumf_openResult(status);
    if (U_FAILURE(*status)) {
        unumf_close(formatter);
        if (result != NULL) unumf_closeResult(result);
        return NULL;
    }

    unumf_formatDecimal(formatter, decimal, decimalLength, result, status);
    unumf_close(formatter);
    if (U_FAILURE(*status)) {
        unumf_closeResult(result);
        return NULL;
    }

    return result;
}

/*
 * Format an exact decimal string according to an ICU number skeleton and locale.
 * On failure, returns the negated UErrorCode (see u_errorName()), never 0 or a positive number.
 */
int32_t fluent_format_decimal(char const* locale, char const* skeleton, char const* decimal,
    int32_t decimalLength, char* formatted, int32_t capacity)
{
    if (formatted == NULL || capacity <= 0) return -U_ILLEGAL_ARGUMENT_ERROR;

    UErrorCode status = U_ZERO_ERROR;
    UFormattedNumber* result =
        fluent_format_decimal_number(locale, skeleton, decimal, decimalLength, &status);
    if (result == NULL) return -(int32_t) status;

    UFormattedValue const* value = unumf_resultAsValue(result, &status);
    int32_t length = 0;
    UChar const* text = ufmtval_getString(value, &length, &status);

    int32_t written = 0;
    u_strToUTF8(formatted, capacity, &written, text, length, &status);
    unumf_closeResult(result);
    if (U_FAILURE(status)) return -(int32_t) status;
    return written;
}

/*
 * Determine the plural category keyword ("zero", "one", "two", "few", "many", "other") for
 * an exact decimal value, formatted per the given number skeleton, in a given locale.
 * Supports both cardinal and ordinal rules.
 * On failure, returns the negated UErrorCode (see u_errorName()), never 0 or a positive number.
 */
int32_t fluent_get_plural_keyword(char const* locale, int ordinal, char const* skeleton,
    char const* decimal, int32_t decimalLength, char* keyword, int32_t capacity)
{
    if (keyword == NULL || capacity <= 0) return -U_ILLEGAL_ARGUMENT_ERROR;

    UErrorCode status = U_ZERO_ERROR;
    UFormattedNumber* result =
        fluent_format_decimal_number(locale, skeleton, decimal, decimalLength, &status);
    if (result == NULL) return -(int32_t) status;

    UPluralRules* rules = uplrules_openForType(
        locale, ordinal ? UPLURAL_TYPE_ORDINAL : UPLURAL_TYPE_CARDINAL, &status);
    if (U_FAILURE(status)) {
        if (rules != NULL) uplrules_close(rules);
        unumf_closeResult(result);
        return -(int32_t) status;
    }

    UChar selected[32];
    int32_t length = uplrules_selectFormatted(rules, result, selected, 32, &status);
    uplrules_close(rules);
    unumf_closeResult(result);
    if (U_FAILURE(status)) return -(int32_t) status;

    int32_t written = 0;
    u_strToUTF8(keyword, capacity, &written, selected, length, &status);
    if (U_FAILURE(status)) return -(int32_t) status;
    return written;
}

/*
 * Resolve a date/time format skeleton (e.g., "yMMMd") into a localised
 * concrete pattern string (e.g., "d MMM y") tailored to the requested locale.
 */
int32_t fluent_resolve_date_pattern(
    char const* locale, char const* skeleton, char* pattern, int32_t capacity)
{
    if (pattern == NULL || capacity <= 0) return -1;

    UErrorCode status = U_ZERO_ERROR;
    UDateTimePatternGenerator* generator = udatpg_open(locale, &status);
    if (U_FAILURE(status)) {
        if (generator != NULL) udatpg_close(generator);
        return -1;
    }

    UChar wanted[64];
    int32_t wantedLength = 0;
    u_strFromUTF8(wanted, 64, &wantedLength, skeleton, -1, &status);
    if (U_FAILURE(status)) {
        udatpg_close(generator);
        return -1;
    }

    UChar best[256];
    int32_t length = udatpg_getBestPattern(generator, wanted, wantedLength, best, 256, &status);
    udatpg_close(generator);
    if (U_FAILURE(status)) return -1;

    int32_t written = 0;
    u_strToUTF8(pattern, capacity, &written, best, length, &status);
    if (U_FAILURE(status)) return -1;
    return written;
}

/*
 * Set the time of an calendar to the specified milliseconds since the Unix epoch.
 */
int32_t fluent_set_calendar_time_ms(UCalendar* calendar, double millis)
{
    if (calendar == NULL) return -1;

    UErrorCode status = U_ZERO_ERROR;
    ucal_setMillis(calendar, millis, &status);
    if (U_FAILURE(status)) return -1;
    return 0;
}

/*
 * The name of an ICU error code (e.g. "U_ILLEGAL_ARGUMENT_ERROR"), as named by u_errorName().
 * The returned string is static and owned by ICU.
 */
char const* fluent_error_name(int32_t code) { return u_errorName((UErrorCode) code); }

/*
 * Determine whether a given locale string is known to ICU.
 */
static UBool fluent_locale_is_available(char const* candidate)
{
    // Binary search assumes that locales returned by uloc_getAvailable are sorted.
    int32_t low = 0;
    int32_t high = uloc_countAvailable() - 1;
    while (low <= high) {
        int32_t mid = low + (high - low) / 2;
        int cmp = strcmp(candidate, uloc_getAvailable(mid));
        if (cmp == 0) return 1;
        if (cmp < 0)
            high = mid - 1;
        else
            low = mid + 1;
    }
    return 0;
}

/*
 * Walk up the BCP 47 locale hierarchy (e.g., de-CH -> de) to find the most specific
 * ancestor that ICU has data for.
 * Matches Intl (ECMA-402) locale-list resolution semantics.
 * Returns -1 if the root is reached without a match.
 */
static int32_t fluent_lookup_locale(char const* start, char* negotiated, int32_t capacity)
{
    UErrorCode status = U_ZERO_ERROR;
    char current[ULOC_FULLNAME_CAPACITY];
    int32_t currentLength = uloc_canonicalize(start, current, ULOC_FULLNAME_CAPACITY, &status);
    if (U_FAILURE(status)) return -1;

    for (;;) {
        if (fluent_locale_is_available(current)) {
            if (currentLength >= capacity) return -1;
            memcpy(negotiated, current, (size_t) currentLength + 1);
            return currentLength;
        }
        if (currentLength == 0) return -1;

        UErrorCode parentStatus = U_ZERO_ERROR;
        char parent[ULOC_FULLNAME_CAPACITY];
        currentLength = uloc_getParent(current, parent, ULOC_FULLNAME_CAPACITY, &parentStatus);
        if (U_FAILURE(parentStatus)) return -1;
        memcpy(current, parent, ULOC_FULLNAME_CAPACITY);
    }
}

/* Negotiate a BCP 47-style locale preference list against the locales ICU has data for,
 * trying each preference's ancestor chain (see fluent_lookup_locale) and taking the first one
 * with any match at all.
 * A NULL entry in `preferences` (Haskell's `Current`) represents ICU's default locale.
 */
int32_t fluent_negotiate_supported_locale(
    char const** preferences, int32_t preferenceCount, char* negotiated, int32_t capacity)
{
    if (negotiated == NULL || capacity <= 0) return -1;

    int32_t written = -1;
    for (int32_t i = 0; i < preferenceCount && written < 0; ++i) {
        written = fluent_lookup_locale(
            preferences[i] ? preferences[i] : uloc_getDefault(), negotiated, capacity);
    }
    return written;
}