// Copyright (c) 2011 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "chrome/common/time_format.h" #include #include "base/lazy_instance.h" #include "base/logging.h" #include "base/memory/scoped_ptr.h" #include "base/memory/scoped_vector.h" #include "base/stl_util.h" #include "base/strings/string16.h" #include "base/strings/utf_string_conversions.h" #include "base/time.h" #include "grit/generated_resources.h" #include "third_party/icu/public/common/unicode/locid.h" #include "third_party/icu/public/i18n/unicode/datefmt.h" #include "third_party/icu/public/i18n/unicode/plurfmt.h" #include "third_party/icu/public/i18n/unicode/plurrule.h" #include "third_party/icu/public/i18n/unicode/smpdtfmt.h" #include "ui/base/l10n/l10n_util.h" using base::Time; using base::TimeDelta; namespace { static const char kFallbackFormatSuffixShort[] = "}"; static const char kFallbackFormatSuffixLeft[] = " left}"; static const char kFallbackFormatSuffixAgo[] = " ago}"; // Contains message IDs for various time units and pluralities. struct MessageIDs { // There are 4 different time units and 6 different pluralities. int ids[4][6]; }; // Message IDs for different time formats. static const MessageIDs kTimeShortMessageIDs = { { { IDS_TIME_SECS_DEFAULT, IDS_TIME_SEC_SINGULAR, IDS_TIME_SECS_ZERO, IDS_TIME_SECS_TWO, IDS_TIME_SECS_FEW, IDS_TIME_SECS_MANY }, { IDS_TIME_MINS_DEFAULT, IDS_TIME_MIN_SINGULAR, IDS_TIME_MINS_ZERO, IDS_TIME_MINS_TWO, IDS_TIME_MINS_FEW, IDS_TIME_MINS_MANY }, { IDS_TIME_HOURS_DEFAULT, IDS_TIME_HOUR_SINGULAR, IDS_TIME_HOURS_ZERO, IDS_TIME_HOURS_TWO, IDS_TIME_HOURS_FEW, IDS_TIME_HOURS_MANY }, { IDS_TIME_DAYS_DEFAULT, IDS_TIME_DAY_SINGULAR, IDS_TIME_DAYS_ZERO, IDS_TIME_DAYS_TWO, IDS_TIME_DAYS_FEW, IDS_TIME_DAYS_MANY } } }; static const MessageIDs kTimeRemainingMessageIDs = { { { IDS_TIME_REMAINING_SECS_DEFAULT, IDS_TIME_REMAINING_SEC_SINGULAR, IDS_TIME_REMAINING_SECS_ZERO, IDS_TIME_REMAINING_SECS_TWO, IDS_TIME_REMAINING_SECS_FEW, IDS_TIME_REMAINING_SECS_MANY }, { IDS_TIME_REMAINING_MINS_DEFAULT, IDS_TIME_REMAINING_MIN_SINGULAR, IDS_TIME_REMAINING_MINS_ZERO, IDS_TIME_REMAINING_MINS_TWO, IDS_TIME_REMAINING_MINS_FEW, IDS_TIME_REMAINING_MINS_MANY }, { IDS_TIME_REMAINING_HOURS_DEFAULT, IDS_TIME_REMAINING_HOUR_SINGULAR, IDS_TIME_REMAINING_HOURS_ZERO, IDS_TIME_REMAINING_HOURS_TWO, IDS_TIME_REMAINING_HOURS_FEW, IDS_TIME_REMAINING_HOURS_MANY }, { IDS_TIME_REMAINING_DAYS_DEFAULT, IDS_TIME_REMAINING_DAY_SINGULAR, IDS_TIME_REMAINING_DAYS_ZERO, IDS_TIME_REMAINING_DAYS_TWO, IDS_TIME_REMAINING_DAYS_FEW, IDS_TIME_REMAINING_DAYS_MANY } } }; static const MessageIDs kTimeRemainingLongMessageIDs = { { { IDS_TIME_REMAINING_SECS_DEFAULT, IDS_TIME_REMAINING_SEC_SINGULAR, IDS_TIME_REMAINING_SECS_ZERO, IDS_TIME_REMAINING_SECS_TWO, IDS_TIME_REMAINING_SECS_FEW, IDS_TIME_REMAINING_SECS_MANY }, { IDS_TIME_REMAINING_LONG_MINS_DEFAULT, IDS_TIME_REMAINING_LONG_MIN_SINGULAR, IDS_TIME_REMAINING_LONG_MINS_ZERO, IDS_TIME_REMAINING_LONG_MINS_TWO, IDS_TIME_REMAINING_LONG_MINS_FEW, IDS_TIME_REMAINING_LONG_MINS_MANY }, { IDS_TIME_REMAINING_HOURS_DEFAULT, IDS_TIME_REMAINING_HOUR_SINGULAR, IDS_TIME_REMAINING_HOURS_ZERO, IDS_TIME_REMAINING_HOURS_TWO, IDS_TIME_REMAINING_HOURS_FEW, IDS_TIME_REMAINING_HOURS_MANY }, { IDS_TIME_REMAINING_DAYS_DEFAULT, IDS_TIME_REMAINING_DAY_SINGULAR, IDS_TIME_REMAINING_DAYS_ZERO, IDS_TIME_REMAINING_DAYS_TWO, IDS_TIME_REMAINING_DAYS_FEW, IDS_TIME_REMAINING_DAYS_MANY } } }; static const MessageIDs kTimeDurationLongMessageIDs = { { { IDS_TIME_DURATION_LONG_SECS_DEFAULT, IDS_TIME_DURATION_LONG_SEC_SINGULAR, IDS_TIME_DURATION_LONG_SECS_ZERO, IDS_TIME_DURATION_LONG_SECS_TWO, IDS_TIME_DURATION_LONG_SECS_FEW, IDS_TIME_DURATION_LONG_SECS_MANY }, { IDS_TIME_DURATION_LONG_MINS_DEFAULT, IDS_TIME_DURATION_LONG_MIN_SINGULAR, IDS_TIME_DURATION_LONG_MINS_ZERO, IDS_TIME_DURATION_LONG_MINS_TWO, IDS_TIME_DURATION_LONG_MINS_FEW, IDS_TIME_DURATION_LONG_MINS_MANY }, { IDS_TIME_HOURS_DEFAULT, IDS_TIME_HOUR_SINGULAR, IDS_TIME_HOURS_ZERO, IDS_TIME_HOURS_TWO, IDS_TIME_HOURS_FEW, IDS_TIME_HOURS_MANY }, { IDS_TIME_DAYS_DEFAULT, IDS_TIME_DAY_SINGULAR, IDS_TIME_DAYS_ZERO, IDS_TIME_DAYS_TWO, IDS_TIME_DAYS_FEW, IDS_TIME_DAYS_MANY } } }; static const MessageIDs kTimeElapsedMessageIDs = { { { IDS_TIME_ELAPSED_SECS_DEFAULT, IDS_TIME_ELAPSED_SEC_SINGULAR, IDS_TIME_ELAPSED_SECS_ZERO, IDS_TIME_ELAPSED_SECS_TWO, IDS_TIME_ELAPSED_SECS_FEW, IDS_TIME_ELAPSED_SECS_MANY }, { IDS_TIME_ELAPSED_MINS_DEFAULT, IDS_TIME_ELAPSED_MIN_SINGULAR, IDS_TIME_ELAPSED_MINS_ZERO, IDS_TIME_ELAPSED_MINS_TWO, IDS_TIME_ELAPSED_MINS_FEW, IDS_TIME_ELAPSED_MINS_MANY }, { IDS_TIME_ELAPSED_HOURS_DEFAULT, IDS_TIME_ELAPSED_HOUR_SINGULAR, IDS_TIME_ELAPSED_HOURS_ZERO, IDS_TIME_ELAPSED_HOURS_TWO, IDS_TIME_ELAPSED_HOURS_FEW, IDS_TIME_ELAPSED_HOURS_MANY }, { IDS_TIME_ELAPSED_DAYS_DEFAULT, IDS_TIME_ELAPSED_DAY_SINGULAR, IDS_TIME_ELAPSED_DAYS_ZERO, IDS_TIME_ELAPSED_DAYS_TWO, IDS_TIME_ELAPSED_DAYS_FEW, IDS_TIME_ELAPSED_DAYS_MANY } } }; // Different format types. enum FormatType { FORMAT_SHORT, FORMAT_REMAINING, FORMAT_REMAINING_LONG, FORMAT_DURATION_LONG, FORMAT_ELAPSED, }; } // namespace class TimeFormatter { public: const std::vector& formatter(FormatType format_type) { switch (format_type) { case FORMAT_SHORT: return short_formatter_.get(); case FORMAT_REMAINING: return time_left_formatter_.get(); case FORMAT_REMAINING_LONG: return time_left_long_formatter_.get(); case FORMAT_DURATION_LONG: return time_duration_long_formatter_.get(); case FORMAT_ELAPSED: return time_elapsed_formatter_.get(); default: NOTREACHED(); return short_formatter_.get(); } } private: static const MessageIDs& GetMessageIDs(FormatType format_type) { switch (format_type) { case FORMAT_SHORT: return kTimeShortMessageIDs; case FORMAT_REMAINING: return kTimeRemainingMessageIDs; case FORMAT_REMAINING_LONG: return kTimeRemainingLongMessageIDs; case FORMAT_DURATION_LONG: return kTimeDurationLongMessageIDs; case FORMAT_ELAPSED: return kTimeElapsedMessageIDs; default: NOTREACHED(); return kTimeShortMessageIDs; } } static const char* GetFallbackFormatSuffix(FormatType format_type) { switch (format_type) { case FORMAT_SHORT: return kFallbackFormatSuffixShort; case FORMAT_REMAINING: case FORMAT_REMAINING_LONG: return kFallbackFormatSuffixLeft; case FORMAT_ELAPSED: return kFallbackFormatSuffixAgo; default: NOTREACHED(); return kFallbackFormatSuffixShort; } } TimeFormatter() { BuildFormats(FORMAT_SHORT, &short_formatter_); BuildFormats(FORMAT_REMAINING, &time_left_formatter_); BuildFormats(FORMAT_REMAINING_LONG, &time_left_long_formatter_); BuildFormats(FORMAT_DURATION_LONG, &time_duration_long_formatter_); BuildFormats(FORMAT_ELAPSED, &time_elapsed_formatter_); } ~TimeFormatter() { } friend struct base::DefaultLazyInstanceTraits; ScopedVector short_formatter_; ScopedVector time_left_formatter_; ScopedVector time_left_long_formatter_; ScopedVector time_duration_long_formatter_; ScopedVector time_elapsed_formatter_; static void BuildFormats(FormatType format_type, ScopedVector* time_formats); static icu::PluralFormat* createFallbackFormat( const icu::PluralRules& rules, int index, FormatType format_type); DISALLOW_COPY_AND_ASSIGN(TimeFormatter); }; static base::LazyInstance g_time_formatter = LAZY_INSTANCE_INITIALIZER; void TimeFormatter::BuildFormats( FormatType format_type, ScopedVector* time_formats) { const icu::UnicodeString kKeywords[] = { UNICODE_STRING_SIMPLE("other"), UNICODE_STRING_SIMPLE("one"), UNICODE_STRING_SIMPLE("zero"), UNICODE_STRING_SIMPLE("two"), UNICODE_STRING_SIMPLE("few"), UNICODE_STRING_SIMPLE("many") }; UErrorCode err = U_ZERO_ERROR; scoped_ptr rules( icu::PluralRules::forLocale(icu::Locale::getDefault(), err)); if (U_FAILURE(err)) { err = U_ZERO_ERROR; icu::UnicodeString fallback_rules("one: n is 1", -1, US_INV); rules.reset(icu::PluralRules::createRules(fallback_rules, err)); DCHECK(U_SUCCESS(err)); } const MessageIDs& message_ids = GetMessageIDs(format_type); for (int i = 0; i < 4; ++i) { icu::UnicodeString pattern; for (size_t j = 0; j < arraysize(kKeywords); ++j) { int msg_id = message_ids.ids[i][j]; std::string sub_pattern = l10n_util::GetStringUTF8(msg_id); // NA means this keyword is not used in the current locale. // Even if a translator translated for this keyword, we do not // use it unless it's 'other' (j=0) or it's defined in the rules // for the current locale. Special-casing of 'other' will be removed // once ICU's isKeyword is fixed to return true for isKeyword('other'). if (sub_pattern.compare("NA") != 0 && (j == 0 || rules->isKeyword(kKeywords[j]))) { pattern += kKeywords[j]; pattern += UNICODE_STRING_SIMPLE("{"); pattern += icu::UnicodeString(sub_pattern.c_str(), "UTF-8"); pattern += UNICODE_STRING_SIMPLE("}"); } } icu::PluralFormat* format = new icu::PluralFormat(*rules, pattern, err); if (U_SUCCESS(err)) { time_formats->push_back(format); } else { delete format; time_formats->push_back(createFallbackFormat(*rules, i, format_type)); // Reset it so that next ICU call can proceed. err = U_ZERO_ERROR; } } } // Create a hard-coded fallback plural format. This will never be called // unless translators make a mistake. icu::PluralFormat* TimeFormatter::createFallbackFormat( const icu::PluralRules& rules, int index, FormatType format_type) { const icu::UnicodeString kUnits[4][2] = { { UNICODE_STRING_SIMPLE("sec"), UNICODE_STRING_SIMPLE("secs") }, { UNICODE_STRING_SIMPLE("min"), UNICODE_STRING_SIMPLE("mins") }, { UNICODE_STRING_SIMPLE("hour"), UNICODE_STRING_SIMPLE("hours") }, { UNICODE_STRING_SIMPLE("day"), UNICODE_STRING_SIMPLE("days") } }; icu::UnicodeString suffix(GetFallbackFormatSuffix(format_type), -1, US_INV); icu::UnicodeString pattern; if (rules.isKeyword(UNICODE_STRING_SIMPLE("one"))) { pattern += UNICODE_STRING_SIMPLE("one{# ") + kUnits[index][0] + suffix; } pattern += UNICODE_STRING_SIMPLE(" other{# ") + kUnits[index][1] + suffix; UErrorCode err = U_ZERO_ERROR; icu::PluralFormat* format = new icu::PluralFormat(rules, pattern, err); DCHECK(U_SUCCESS(err)); return format; } static string16 FormatTimeImpl(const TimeDelta& delta, FormatType format_type) { if (delta.ToInternalValue() < 0) { NOTREACHED() << "Negative duration"; return string16(); } int number; const std::vector& formatters = g_time_formatter.Get().formatter(format_type); UErrorCode error = U_ZERO_ERROR; icu::UnicodeString time_string; // Less than a minute gets "X seconds left" if (delta.ToInternalValue() < Time::kMicrosecondsPerMinute) { number = static_cast(delta.ToInternalValue() / Time::kMicrosecondsPerSecond); time_string = formatters[0]->format(number, error); // Less than 1 hour gets "X minutes left". } else if (delta.ToInternalValue() < Time::kMicrosecondsPerHour) { number = static_cast(delta.ToInternalValue() / Time::kMicrosecondsPerMinute); time_string = formatters[1]->format(number, error); // Less than 1 day remaining gets "X hours left" } else if (delta.ToInternalValue() < Time::kMicrosecondsPerDay) { number = static_cast(delta.ToInternalValue() / Time::kMicrosecondsPerHour); time_string = formatters[2]->format(number, error); // Anything bigger gets "X days left" } else { number = static_cast(delta.ToInternalValue() / Time::kMicrosecondsPerDay); time_string = formatters[3]->format(number, error); } // With the fallback added, this should never fail. DCHECK(U_SUCCESS(error)); int capacity = time_string.length() + 1; DCHECK_GT(capacity, 1); string16 result; time_string.extract(static_cast(WriteInto(&result, capacity)), capacity, error); DCHECK(U_SUCCESS(error)); return result; } // static string16 TimeFormat::TimeElapsed(const TimeDelta& delta) { return FormatTimeImpl(delta, FORMAT_ELAPSED); } // static string16 TimeFormat::TimeRemaining(const TimeDelta& delta) { return FormatTimeImpl(delta, FORMAT_REMAINING); } // static string16 TimeFormat::TimeRemainingLong(const TimeDelta& delta) { return FormatTimeImpl(delta, FORMAT_REMAINING_LONG); } // static string16 TimeFormat::TimeRemainingShort(const TimeDelta& delta) { return FormatTimeImpl(delta, FORMAT_SHORT); } // static string16 TimeFormat::TimeDurationLong(const TimeDelta& delta) { return FormatTimeImpl(delta, FORMAT_DURATION_LONG); } // static string16 TimeFormat::RelativeDate( const Time& time, const Time* optional_midnight_today) { Time midnight_today = optional_midnight_today ? *optional_midnight_today : Time::Now().LocalMidnight(); TimeDelta day = TimeDelta::FromMicroseconds(Time::kMicrosecondsPerDay); Time tomorrow = midnight_today + day; Time yesterday = midnight_today - day; if (time >= tomorrow) return string16(); else if (time >= midnight_today) return l10n_util::GetStringUTF16(IDS_PAST_TIME_TODAY); else if (time >= yesterday) return l10n_util::GetStringUTF16(IDS_PAST_TIME_YESTERDAY); return string16(); }