File indexing completed on 2024-06-02 05:18:44

0001 /*
0002     SPDX-FileCopyrightText: 2018 Volker Krause <vkrause@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "localizer.h"
0008 
0009 #include <KItinerary/JsonLdDocument>
0010 #include <KItinerary/Place>
0011 #include <KItinerary/PriceUtil>
0012 
0013 #include <KContacts/Address>
0014 
0015 #include <KCountry>
0016 #include <KFormat>
0017 #include <KLocalizedString>
0018 
0019 #include <QDateTime>
0020 #include <QLocale>
0021 #include <QMetaProperty>
0022 #include <QTimeZone>
0023 
0024 #ifdef Q_OS_ANDROID
0025 #include "kandroidextras/javatypes.h"
0026 #include "kandroidextras/jnisignature.h"
0027 #include "kandroidextras/javalocale.h"
0028 
0029 using namespace KAndroidExtras;
0030 #endif
0031 
0032 #include <cmath>
0033 #include <cstring>
0034 
0035 using namespace KItinerary;
0036 
0037 static QString readFromGadget(const QMetaObject *mo, const QVariant &gadget, const char *propName)
0038 {
0039     const auto propIdx = mo->indexOfProperty(propName);
0040     if (propIdx < 0) {
0041         return {};
0042     }
0043     const auto prop = mo->property(propIdx);
0044     if (!prop.isValid()) {
0045         return {};
0046     }
0047     return prop.readOnGadget(gadget.constData()).toString();
0048 }
0049 
0050 static KContacts::Address variantToKContactsAddress(const QVariant &obj)
0051 {
0052     KContacts::Address address;
0053     if (JsonLd::isA<PostalAddress>(obj)) {
0054         const auto a = obj.value<PostalAddress>();
0055         address.setStreet(a.streetAddress());
0056         address.setPostalCode(a.postalCode());
0057         address.setLocality(a.addressLocality());
0058         address.setRegion(a.addressRegion());
0059         address.setCountry(a.addressCountry());
0060     } else if (std::strcmp(obj.typeName(), "KOSMIndoorMap::OSMAddress") == 0) {
0061         const auto mo = QMetaType(obj.userType()).metaObject();
0062         address.setStreet(readFromGadget(mo, obj, "street") + QLatin1Char(' ') + readFromGadget(mo, obj, "houseNumber"));
0063         address.setPostalCode(readFromGadget(mo, obj, "postalCode"));
0064         address.setLocality(readFromGadget(mo, obj, "city"));
0065         address.setRegion(readFromGadget(mo, obj, "state"));
0066         address.setCountry(readFromGadget(mo, obj, "country"));
0067     }
0068     return address;
0069 }
0070 
0071 QString Localizer::formatAddress(const QVariant &obj) const
0072 {
0073     KContacts::Address address = variantToKContactsAddress(obj);
0074     if (address.isEmpty()) {
0075         return {};
0076     }
0077     return address.formatted(KContacts::AddressFormatStyle::MultiLineInternational);
0078 }
0079 
0080 static bool addressEmptyExceptForCountry(const KContacts::Address &address)
0081 {
0082     return address.street().isEmpty() && address.locality().isEmpty()
0083             && address.postalCode().isEmpty() && address.region().isEmpty()
0084             && !address.country().isEmpty();
0085 }
0086 
0087 QString Localizer::formatAddressWithContext(const QVariant &obj, const QVariant &otherObj, const QString &homeCountryIsoCode)
0088 {
0089     const KContacts::Address address = variantToKContactsAddress(obj);
0090 
0091     if (address.isEmpty()) {
0092         return {};
0093     }
0094 
0095     const bool includeCountry = [&] {
0096         if (homeCountryIsoCode.isEmpty()) {
0097             return true;
0098         }
0099 
0100         if (!addressEmptyExceptForCountry(address)) {
0101             return true;
0102         }
0103 
0104         if (address.country() != homeCountryIsoCode) {
0105             return true;
0106         }
0107 
0108         const KContacts::Address otherAddress = variantToKContactsAddress(otherObj);
0109         if (!otherAddress.country().isEmpty() && address.country() != otherAddress.country()) {
0110             return true;
0111         }
0112 
0113         return false;
0114     }();
0115 
0116     if (includeCountry) {
0117         return address.formatted(KContacts::AddressFormatStyle::MultiLineInternational);
0118     } else {
0119         return address.formatted(KContacts::AddressFormatStyle::MultiLineDomestic);
0120     }
0121 }
0122 
0123 static bool needsTimeZone(const QDateTime &dt)
0124 {
0125     if (dt.timeSpec() == Qt::TimeZone && dt.timeZone().abbreviation(dt) != QTimeZone::systemTimeZone().abbreviation(dt)) {
0126         return true;
0127     } else if (dt.timeSpec() == Qt::OffsetFromUTC && dt.timeZone().offsetFromUtc(dt) != dt.offsetFromUtc()) {
0128         return true;
0129     } else if (dt.timeSpec() == Qt::UTC && QTimeZone::systemTimeZone() != QTimeZone::utc()) {
0130         return true;
0131     }
0132     return false;
0133 }
0134 
0135 static QString tzAbbreviation(const QDateTime &dt)
0136 {
0137     const auto tz = dt.timeZone();
0138 
0139 #ifdef Q_OS_ANDROID
0140     // the QTimeZone backend implementation on Android isn't as complete as the desktop ones, so we need to do this ourselves here
0141     // eventually, this should be upstreamed to Qt
0142     auto abbr = QJniObject::callStaticObjectMethod("org/kde/itinerary/QTimeZone", "abbreviation",
0143                     Jni::signature<java::lang::String(java::lang::String, jlong, java::util::Locale, bool)>(),
0144                     QJniObject::fromString(QString::fromUtf8(tz.id())).object(), dt.toMSecsSinceEpoch(),
0145                     KAndroidExtras::Locale::current().object(), tz.isDaylightTime(dt)).toString();
0146 
0147     if (!abbr.isEmpty()) {
0148         return abbr;
0149     }
0150 #endif
0151 
0152     return tz.abbreviation(dt);
0153 }
0154 
0155 QString Localizer::formatTime(const QVariant &obj, const QString &propertyName) const
0156 {
0157     const auto dt = JsonLdDocument::readProperty(obj, propertyName.toUtf8().constData()).toDateTime();
0158     if (!dt.isValid()) {
0159         return {};
0160     }
0161 
0162     QString output;
0163     if (QLocale().timeFormat(QLocale::ShortFormat).contains(QStringLiteral("ss"))) {
0164         output = QLocale().toString(dt.time(), QStringLiteral("hh:mm"));
0165     } else {
0166         output = QLocale().toString(dt.time(), QLocale::ShortFormat);
0167     }
0168     if (needsTimeZone(dt)) {
0169         output += QLatin1Char(' ') + tzAbbreviation(dt);
0170     }
0171     return output;
0172 }
0173 
0174 QString Localizer::formatDate(const QVariant &obj, const QString &propertyName) const
0175 {
0176     const auto dt = JsonLdDocument::readProperty(obj, propertyName.toUtf8().constData()).toDate();
0177     if (!dt.isValid()) {
0178         return {};
0179     }
0180 
0181     if (dt.year() <= 1900) { // no year specified
0182         return dt.toString(i18nc("day-only date format", "dd MMMM"));
0183     }
0184     return QLocale().toString(dt, QLocale::ShortFormat);
0185 }
0186 
0187 QString Localizer::formatDateTime(const QVariant& obj, const QString& propertyName) const
0188 {
0189     const auto dt = JsonLdDocument::readProperty(obj, propertyName.toUtf8().constData()).toDateTime();
0190     if (!dt.isValid()) {
0191         return {};
0192     }
0193 
0194     auto s = QLocale().toString(dt, QLocale::ShortFormat);
0195     if (needsTimeZone(dt)) {
0196         s += QLatin1Char(' ') + tzAbbreviation(dt);
0197     }
0198     return s;
0199 }
0200 
0201 QString Localizer::formatDateOrDateTimeLocal(const QVariant& obj, const QString& propertyName) const
0202 {
0203     const auto dt = JsonLdDocument::readProperty(obj, propertyName.toUtf8().constData()).toDateTime();
0204     if (!dt.isValid()) {
0205         return {};
0206     }
0207 
0208     // detect likely date-only values
0209     if (dt.timeSpec() == Qt::LocalTime && (dt.time() == QTime{0, 0, 0} || dt.time() == QTime{23, 59, 59})) {
0210         return QLocale().toString(dt.date(), QLocale::ShortFormat);
0211     }
0212 
0213     return QLocale().toString(dt.toLocalTime(), QLocale::ShortFormat);
0214 }
0215 
0216 QString Localizer::formatDuration(int seconds) const
0217 {
0218     if (seconds < 0) {
0219         return QLocale().negativeSign() + KFormat().formatDuration((-seconds * 1000), KFormat::HideSeconds);
0220     }
0221     return KFormat().formatDuration(seconds * 1000, KFormat::HideSeconds);
0222 }
0223 
0224 QString Localizer::formatDistance(int meter)
0225 {
0226     if (meter < 1000) {
0227         return i18nc("distance in meter", "%1 m", meter);
0228     }
0229     if (meter < 10000) {
0230         return i18nc("distance in kilometer", "%1 km", ((int)meter/100)/10.0);
0231     }
0232     return i18nc("distance in kilometer", "%1 km", (int)qRound(meter/1000.0));
0233 }
0234 
0235 QString Localizer::formatSpeed(int km_per_hour)
0236 {
0237     // TODO locale-specific unit conversion
0238     return i18nc("speed in kilometers per hour", "%1 km/h", km_per_hour);
0239 }
0240 
0241 QString Localizer::formatWeight(int gram)
0242 {
0243     if (gram < 1000) {
0244         return i18nc("weight in gram", "%1 g", gram);
0245     }
0246     if (gram < 10000) {
0247         return i18nc("weight in kilogram", "%1 kg", ((int)gram/100)/10.0);
0248     }
0249     return i18nc("weight in kilogram", "%1 kg", (int)qRound(gram/1000.0));
0250 
0251 }
0252 
0253 QString Localizer::formatTemperature(double temperature)
0254 {
0255     return i18nc("temperature", "%1°C", (int)qRound(temperature));
0256 }
0257 
0258 QString Localizer::formatCurrency(double value, const QString &isoCode)
0259 {
0260     const auto decimalCount = PriceUtil::decimalCount(isoCode);
0261 
0262     // special case for displaying conversion rates (which can be very small)
0263     // and thus need a higher precision than regular values
0264     double i = 0.0;
0265     double f = std::modf(value * std::pow(10, decimalCount), &i);
0266     if (i == 0.0 && f > 0.0) {
0267         return QLocale().toCurrencyString(value, isoCode);
0268     }
0269 
0270     return QLocale().toCurrencyString(value, isoCode, decimalCount);
0271 }
0272 
0273 #include "moc_localizer.cpp"