hrev47971 adds 3 changesets to branch 'master' old head: c9f5d71c97e6a56a81ce3091a581f7a3261623f2 new head: 53382a8aa7d12cc7d72e36acd6ac7c70429eddf0 overview: http://cgit.haiku-os.org/haiku/log/?qt=range&q=53382a8+%5Ec9f5d71 ---------------------------------------------------------------------------- 699ddf4: DateFormatTest: fix after ICU upgrade * Timezone names have changed. Test the medium time format which doesn't include them, as we aren't forcing a specific one. * French date format also changed to use 4 digit year name. 0da7796: Add BMessageFormat class. This can be used to format complex messages properly. It moves the complexity of handling plural forms, gender, and anything else needed into the localizable string, rather than hardcoding it in the code. This moves the difficulty of handling these things properly to people doing translations, rather than relying on developers to do it. Fixes #10755, but our localization must now be updated to make use of the feature. 53382a8: Start making use of BMessageFormat. [ Adrien Destugues <pulkomandy@xxxxxxxxx> ] ---------------------------------------------------------------------------- 15 files changed, 230 insertions(+), 109 deletions(-) headers/os/locale/MessageFormat.h | 19 +++++ .../inbound_filters/notifier/filter.cpp | 26 +++---- src/apps/aboutsystem/AboutSystem.cpp | 14 ++-- src/apps/pairs/PairsWindow.cpp | 14 ++-- src/kits/locale/Jamfile | 1 + src/kits/locale/MessageFormat.cpp | 41 +++++++++++ src/kits/tracker/CountView.cpp | 11 ++- src/kits/tracker/InfoWindow.cpp | 38 +++------- src/servers/mail/DeskbarView.cpp | 18 ++--- src/servers/mail/MailDaemon.cpp | 42 ++++------- src/tests/kits/locale/DateFormatTest.cpp | 12 ++-- src/tests/kits/locale/Jamfile | 1 + src/tests/kits/locale/LocaleKitTestAddon.cpp | 2 + src/tests/kits/locale/MessageFormatTest.cpp | 76 ++++++++++++++++++++ src/tests/kits/locale/MessageFormatTest.h | 24 +++++++ ############################################################################ Commit: 699ddf447c89331a9927f8e1132159e7737f481d URL: http://cgit.haiku-os.org/haiku/commit/?id=699ddf4 Author: Adrien Destugues <pulkomandy@xxxxxxxxx> Date: Tue Oct 7 06:49:45 2014 UTC DateFormatTest: fix after ICU upgrade * Timezone names have changed. Test the medium time format which doesn't include them, as we aren't forcing a specific one. * French date format also changed to use 4 digit year name. ---------------------------------------------------------------------------- diff --git a/src/tests/kits/locale/DateFormatTest.cpp b/src/tests/kits/locale/DateFormatTest.cpp index 201d2d1..2575eaf 100644 --- a/src/tests/kits/locale/DateFormatTest.cpp +++ b/src/tests/kits/locale/DateFormatTest.cpp @@ -40,11 +40,11 @@ DateFormatTest::TestFormat() static const Value values[] = { {"en", "en_US", 12345, "1/1/70", "January 1, 1970", - "4:25 AM", "4:25:45 AM CET"}, - {"fr", "fr_FR", 12345, "01/01/70", "1 janvier 1970", - "04:25", "04:25:45 HNEC"}, - {"fr", "fr_FR", 12345678, "23/05/70", "23 mai 1970", - "22:21", "22:21:18 HNEC"}, + "4:25 AM", "4:25:45 AM"}, + {"fr", "fr_FR", 12345, "01/01/1970", "1 janvier 1970", + "04:25", "04:25:45"}, + {"fr", "fr_FR", 12345678, "23/05/1970", "23 mai 1970", + "22:21", "22:21:18"}, {NULL} }; @@ -71,7 +71,7 @@ DateFormatTest::TestFormat() CPPUNIT_ASSERT_EQUAL(B_OK, result); CPPUNIT_ASSERT_EQUAL(BString(values[i].shortTime), output); - result = timeFormat.Format(output, values[i].time, B_LONG_TIME_FORMAT); + result = timeFormat.Format(output, values[i].time, B_MEDIUM_TIME_FORMAT); CPPUNIT_ASSERT_EQUAL(B_OK, result); CPPUNIT_ASSERT_EQUAL(BString(values[i].longTime), output); } ############################################################################ Commit: 0da7796e6c95de168ac77151a520c853c120a968 URL: http://cgit.haiku-os.org/haiku/commit/?id=0da7796 Author: Adrien Destugues <pulkomandy@xxxxxxxxx> Date: Tue Oct 7 06:51:23 2014 UTC Ticket: https://dev.haiku-os.org/ticket/10755 Add BMessageFormat class. This can be used to format complex messages properly. It moves the complexity of handling plural forms, gender, and anything else needed into the localizable string, rather than hardcoding it in the code. This moves the difficulty of handling these things properly to people doing translations, rather than relying on developers to do it. Fixes #10755, but our localization must now be updated to make use of the feature. ---------------------------------------------------------------------------- diff --git a/headers/os/locale/MessageFormat.h b/headers/os/locale/MessageFormat.h new file mode 100644 index 0000000..ff2a9c2 --- /dev/null +++ b/headers/os/locale/MessageFormat.h @@ -0,0 +1,19 @@ +/* + * Copyright 2014, Haiku, Inc. + * Distributed under the terms of the MIT License. + */ +#ifndef _B_MESSAGE_FORMAT_H_ +#define _B_MESSAGE_FORMAT_H_ + + +#include <Format.h> + + +class BMessageFormat: public BFormat { +public: + status_t Format(BString& buffer, const BString message, + const int32 arg); +}; + + +#endif diff --git a/src/kits/locale/Jamfile b/src/kits/locale/Jamfile index 5b3f509..c21abba 100644 --- a/src/kits/locale/Jamfile +++ b/src/kits/locale/Jamfile @@ -27,6 +27,7 @@ local sources = DateFormat.cpp DateTimeFormat.cpp DurationFormat.cpp + MessageFormat.cpp NumberFormat.cpp TimeFormat.cpp TimeUnitFormat.cpp diff --git a/src/kits/locale/MessageFormat.cpp b/src/kits/locale/MessageFormat.cpp new file mode 100644 index 0000000..b5af3ec --- /dev/null +++ b/src/kits/locale/MessageFormat.cpp @@ -0,0 +1,41 @@ +/* + * Copyright 2014, Haiku, Inc. + * Distributed under the terms of the MIT License. + */ +#include <MessageFormat.h> + +#include <FormattingConventionsPrivate.h> +#include <LanguagePrivate.h> + +#include <ICUWrapper.h> + +#include <unicode/msgfmt.h> + + +status_t +BMessageFormat::Format(BString& output, const BString message, const int32 arg) +{ + UnicodeString buffer; + UErrorCode error = U_ZERO_ERROR; + + Formattable arguments[] = { + (int32_t)arg + }; + + Locale* icuLocale + = fConventions.UseStringsFromPreferredLanguage() + ? BLanguage::Private(&fLanguage).ICULocale() + : BFormattingConventions::Private(&fConventions).ICULocale(); + + MessageFormat formatter(UnicodeString::fromUTF8(message.String()), + *icuLocale, error); + FieldPosition pos; + buffer = formatter.format(arguments, 1, buffer, pos, error); + if (!U_SUCCESS(error)) + return B_ERROR; + + BStringByteSink byteSink(&output); + buffer.toUTF8(byteSink); + + return B_OK; +} diff --git a/src/tests/kits/locale/Jamfile b/src/tests/kits/locale/Jamfile index af5166c..2fd68b6 100644 --- a/src/tests/kits/locale/Jamfile +++ b/src/tests/kits/locale/Jamfile @@ -52,6 +52,7 @@ UnitTestLib localekittest.so : DateFormatTest.cpp DurationFormatTest.cpp LanguageTest.cpp + MessageFormatTest.cpp UnicodeCharTest.cpp : be [ TargetLibstdc++ ] diff --git a/src/tests/kits/locale/LocaleKitTestAddon.cpp b/src/tests/kits/locale/LocaleKitTestAddon.cpp index 460f32f..2d04a79 100644 --- a/src/tests/kits/locale/LocaleKitTestAddon.cpp +++ b/src/tests/kits/locale/LocaleKitTestAddon.cpp @@ -11,6 +11,7 @@ #include "DateFormatTest.h" #include "DurationFormatTest.h" #include "LanguageTest.h" +#include "MessageFormatTest.h" #include "UnicodeCharTest.h" @@ -23,6 +24,7 @@ getTestSuite() DateFormatTest::AddTests(*suite); DurationFormatTest::AddTests(*suite); LanguageTest::AddTests(*suite); + MessageFormatTest::AddTests(*suite); UnicodeCharTest::AddTests(*suite); return suite; diff --git a/src/tests/kits/locale/MessageFormatTest.cpp b/src/tests/kits/locale/MessageFormatTest.cpp new file mode 100644 index 0000000..65407cb --- /dev/null +++ b/src/tests/kits/locale/MessageFormatTest.cpp @@ -0,0 +1,76 @@ +/* + * Copyright 2014 Haiku, Inc. + * Distributed under the terms of the MIT License. + */ + + +#include "MessageFormatTest.h" + +#include <Locale.h> +#include <MessageFormat.h> + +#include <cppunit/TestCaller.h> +#include <cppunit/TestSuite.h> + + +MessageFormatTest::MessageFormatTest() +{ +} + + +MessageFormatTest::~MessageFormatTest() +{ +} + + +void +MessageFormatTest::TestFormat() +{ + BString output; + BMessageFormat formatter; + + struct Test { + const char* locale; + const char* pattern; + int32 number; + const char* expected; + }; + + static const char* polishTemplate = "{0, plural, one{Wybrano # obiekt} " + "few{Wybrano # obiekty} many{Wybrano # obiektów} " + "other{Wybrano # obyektu}}"; + + static const Test tests[] = { + {"en_US", "{0, plural, one{# dog} other{# dogs}}", 1, "1 dog"}, + {"en_US", "{0, plural, one{# dog} other{# dogs}}", 2, "2 dogs"}, + {"pl_PL", polishTemplate, 1, "Wybrano 1 obiekt"}, + {"pl_PL", polishTemplate, 3, "Wybrano 3 obyektu"}, + {"pl_PL", polishTemplate, 5, "Wybrano 5 obyektu"}, + {"pl_PL", polishTemplate, 23, "Wybrano 23 obyektu"}, + {NULL, NULL, 0, NULL} + }; + + for (int i = 0; tests[i].pattern != NULL; i++) { + status_t result; + NextSubTest(); + output.Truncate(0); + BLanguage language(tests[i].locale); + formatter.SetLanguage(language); + + result = formatter.Format(output, tests[i].pattern, tests[i].number); + CPPUNIT_ASSERT_EQUAL(B_OK, result); + CPPUNIT_ASSERT_EQUAL(BString(tests[i].expected), output); + } +} + + +/*static*/ void +MessageFormatTest::AddTests(BTestSuite& parent) +{ + CppUnit::TestSuite& suite = *new CppUnit::TestSuite("MessageFormatTest"); + + suite.addTest(new CppUnit::TestCaller<MessageFormatTest>( + "MessageFormatTest::TestFormat", &MessageFormatTest::TestFormat)); + + parent.addTest("MessageFormatTest", &suite); +} diff --git a/src/tests/kits/locale/MessageFormatTest.h b/src/tests/kits/locale/MessageFormatTest.h new file mode 100644 index 0000000..924f679 --- /dev/null +++ b/src/tests/kits/locale/MessageFormatTest.h @@ -0,0 +1,24 @@ +/* + * Copyright 2014 Haiku, Inc. + * Distributed under the terms of the MIT License. + */ +#ifndef MESSAGE_FORMAT_TEST_H +#define MESSAGE_FORMAT_TEST_H + + +#include <TestCase.h> +#include <TestSuite.h> + + +class MessageFormatTest: public BTestCase { +public: + MessageFormatTest(); + virtual ~MessageFormatTest(); + + void TestFormat(); + + static void AddTests(BTestSuite& suite); +}; + + +#endif ############################################################################ Revision: hrev47971 Commit: 53382a8aa7d12cc7d72e36acd6ac7c70429eddf0 URL: http://cgit.haiku-os.org/haiku/commit/?id=53382a8 Author: Adrien Destugues <pulkomandy@xxxxxxxxx> Date: Tue Oct 7 07:26:45 2014 UTC Start making use of BMessageFormat. ---------------------------------------------------------------------------- diff --git a/src/add-ons/mail_daemon/inbound_filters/notifier/filter.cpp b/src/add-ons/mail_daemon/inbound_filters/notifier/filter.cpp index d417ed7..1ad15eb 100644 --- a/src/add-ons/mail_daemon/inbound_filters/notifier/filter.cpp +++ b/src/add-ons/mail_daemon/inbound_filters/notifier/filter.cpp @@ -10,6 +10,7 @@ #include <Beep.h> #include <Catalog.h> #include <Message.h> +#include <MessageFormat.h> #include <Path.h> #include <String.h> @@ -67,15 +68,12 @@ NotifyFilter::MailboxSynced(status_t status) system_beep("New E-mail"); if (fStrategy & alert) { - BString text, numString; - if (fNNewMessages != 1) - text << B_TRANSLATE("You have %num new messages for %name."); - else - text << B_TRANSLATE("You have %num new message for %name."); + BString text; + BMessageFormat().Format(text, B_TRANSLATE( + "You have {0, plural, one{# new message} other{# new messages}} " + "for %account."), fNNewMessages); - numString << fNNewMessages; - text.ReplaceFirst("%num", numString); - text.ReplaceFirst("%name", fMailProtocol.AccountSettings().Name()); + text.ReplaceFirst("%account", fMailProtocol.AccountSettings().Name()); BAlert *alert = new BAlert(B_TRANSLATE("New messages"), text.String(), B_TRANSLATE("OK"), NULL, NULL, B_WIDTH_AS_USUAL); @@ -99,14 +97,10 @@ NotifyFilter::MailboxSynced(status_t status) } if (fStrategy & log_window) { - BString message, numString; - if (fNNewMessages != 1) - message << B_TRANSLATE("%num new messages"); - else - message << B_TRANSLATE("%num new message"); - - numString << fNNewMessages; - message.ReplaceFirst("%num", numString); + BString message; + BMessageFormat().Format(message, B_TRANSLATE( + "{0, plural, one{# new message} other{# new messages}}"), + fNNewMessages); fMailProtocol.ShowMessage(message.String()); } diff --git a/src/apps/aboutsystem/AboutSystem.cpp b/src/apps/aboutsystem/AboutSystem.cpp index ea834d1..fd1a68b 100644 --- a/src/apps/aboutsystem/AboutSystem.cpp +++ b/src/apps/aboutsystem/AboutSystem.cpp @@ -29,6 +29,7 @@ #include <Font.h> #include <fs_attr.h> #include <LayoutBuilder.h> +#include <MessageFormat.h> #include <MessageRunner.h> #include <Messenger.h> #include <OS.h> @@ -442,13 +443,10 @@ AboutView::AboutView() B_ALIGN_VERTICAL_UNSET)); // CPU count, type and clock speed - char processorLabel[256]; - if (systemInfo.cpu_count > 1) { - snprintf(processorLabel, sizeof(processorLabel), - B_TRANSLATE("%ld Processors:"), systemInfo.cpu_count); - } else - strlcpy(processorLabel, B_TRANSLATE("Processor:"), - sizeof(processorLabel)); + BString processorLabel; + BMessageFormat().Format(processorLabel, B_TRANSLATE_COMMENT( + "{0, plural, one{Processors:} other{# Processors:}}", + "\"Processor:\" or \"2 Processors:\""), systemInfo.cpu_count); uint32 topologyNodeCount = 0; cpu_topology_node_info* topology = NULL; @@ -550,7 +548,7 @@ AboutView::AboutView() .Add(versionView) .Add(abiView) .AddStrut(offset) - .Add(_CreateLabel("cpulabel", processorLabel)) + .Add(_CreateLabel("cpulabel", processorLabel.String())) .Add(cpuView) .Add(frequencyView) .AddStrut(offset) diff --git a/src/apps/pairs/PairsWindow.cpp b/src/apps/pairs/PairsWindow.cpp index 639d883..8a198a2 100644 --- a/src/apps/pairs/PairsWindow.cpp +++ b/src/apps/pairs/PairsWindow.cpp @@ -22,6 +22,7 @@ #include <Menu.h> #include <MenuBar.h> #include <MenuItem.h> +#include <MessageFormat.h> #include <MessageRunner.h> #include <String.h> #include <TextView.h> @@ -294,17 +295,20 @@ PairsWindow::MessageReceived(BMessage* message) // game end and results if (fFinishPairs == pairsButtonList->CountItems() / 2) { - BString score; - score << fButtonClicks; BString strAbout = B_TRANSLATE("%app%\n" "\twritten by Ralf Schülke\n" "\tCopyright 2008-2010, Haiku Inc.\n" - "\n" - "You completed the game in %num% clicks.\n"); + "\n"); strAbout.ReplaceFirst("%app%", B_TRANSLATE_SYSTEM_NAME("Pairs")); - strAbout.ReplaceFirst("%num%", score); + + // Note: in english the singular form is never used, but other + // languages behave differently. + BMessageFormat().Format(strAbout, B_TRANSLATE( + "You completed the game in " + "{0, plural, one{# click} other{# clicks}}.\n"), + fButtonClicks); BAlert* alert = new BAlert("about", strAbout.String(), diff --git a/src/kits/tracker/CountView.cpp b/src/kits/tracker/CountView.cpp index 711ebde..195ac4f 100644 --- a/src/kits/tracker/CountView.cpp +++ b/src/kits/tracker/CountView.cpp @@ -41,6 +41,7 @@ All rights reserved. #include <Catalog.h> #include <ControlLook.h> #include <Locale.h> +#include <MessageFormat.h> #include "AutoLock.h" #include "Bitmaps.h" @@ -230,13 +231,11 @@ BCountView::Draw(BRect updateRect) } else { if (fLastCount == 0) itemString << B_TRANSLATE("no items"); - else if (fLastCount == 1) - itemString << B_TRANSLATE("1 item"); else { - itemString.SetTo(B_TRANSLATE("%num items")); - char numString[256]; - snprintf(numString, sizeof(numString), "%" B_PRId32, fLastCount); - itemString.ReplaceFirst("%num", numString); + BMessageFormat().Format(itemString, B_TRANSLATE_COMMENT( + "{0, plural, one{# item} other{# items}}", + "Number of selected items: \"1 item\" or \"2 items\""), + fLastCount); } } diff --git a/src/kits/tracker/InfoWindow.cpp b/src/kits/tracker/InfoWindow.cpp index 37061ec..0e311e5 100644 --- a/src/kits/tracker/InfoWindow.cpp +++ b/src/kits/tracker/InfoWindow.cpp @@ -47,6 +47,7 @@ All rights reserved. #include <Font.h> #include <Locale.h> #include <MenuField.h> +#include <MessageFormat.h> #include <Mime.h> #include <NodeInfo.h> #include <NodeMonitor.h> @@ -675,43 +676,22 @@ void BInfoWindow::GetSizeString(BString &result, off_t size, int32 fileCount) { char sizeBuffer[128]; + BMessageFormat messageFormat; + result << string_for_size((double)size, sizeBuffer, sizeof(sizeBuffer)); - // when we show the byte size, format it with a thousands delimiter - // TODO: use BCountry::FormatNumber if (size >= kKBSize) { - char numStr[128]; - snprintf(numStr, sizeof(numStr), "%" B_PRIdOFF, size); - BString bytes; - - uint32 length = strlen(numStr); - if (length >= 4) { - uint32 charsTillComma = length % 3; - if (charsTillComma == 0) - charsTillComma = 3; - - uint32 numberIndex = 0; - - while (numStr[numberIndex]) { - bytes += numStr[numberIndex++]; - if (--charsTillComma == 0 && numStr[numberIndex]) { - bytes += ','; - charsTillComma = 3; - } - } - } - - result << " " << B_TRANSLATE("(%bytes bytes)"); + result << " "; + messageFormat.Format(result, B_TRANSLATE( + "{0, plural, one{(# byte)} other{(# bytes)}}"), size); // "bytes" translation could come from string_for_size // which could be part of the localekit itself - result.ReplaceFirst("%bytes", bytes); } if (fileCount != 0) { - result << " " << B_TRANSLATE("for %num files"); - BString countString; - countString << fileCount; - result.ReplaceFirst("%num", countString); + result << " "; + messageFormat.Format(result, B_TRANSLATE( + "{0, plural, one{for # file} other{for # files}}"), fileCount); } } diff --git a/src/servers/mail/DeskbarView.cpp b/src/servers/mail/DeskbarView.cpp index eca435c..520c5b9 100644 --- a/src/servers/mail/DeskbarView.cpp +++ b/src/servers/mail/DeskbarView.cpp @@ -19,6 +19,7 @@ #include <kernel/fs_info.h> #include <kernel/fs_index.h> #include <MenuItem.h> +#include <MessageFormat.h> #include <Messenger.h> #include <NodeInfo.h> #include <NodeMonitor.h> @@ -525,7 +526,7 @@ DeskbarView::_BuildMenu() item = new BMenuItem(path.Leaf(), msg); menu->AddItem(item); - if(entry.InitCheck() != B_OK) + if (entry.InitCheck() != B_OK) item->SetEnabled(false); } if (count > 0) @@ -541,14 +542,10 @@ DeskbarView::_BuildMenu() // The New E-mail query if (fNewMessages > 0) { - BString string, numString; - if (fNewMessages != 1) - string << B_TRANSLATE("%num new messages"); - else - string << B_TRANSLATE("%num new message"); - - numString << fNewMessages; - string.ReplaceFirst("%num", numString); + BString string; + BMessageFormat().Format(string, B_TRANSLATE( + "{0, plural, one{# new message} other{# new messages}}"), + fNewMessages); _GetNewQueryRef(ref); @@ -559,8 +556,7 @@ DeskbarView::_BuildMenu() navMenu->SetNavDir(&ref); menu->AddItem(item); - } - else { + } else { menu->AddItem(item = new BMenuItem(B_TRANSLATE("No new messages"), NULL)); item->SetEnabled(false); diff --git a/src/servers/mail/MailDaemon.cpp b/src/servers/mail/MailDaemon.cpp index 05586c5..f1e5dab 100644 --- a/src/servers/mail/MailDaemon.cpp +++ b/src/servers/mail/MailDaemon.cpp @@ -20,6 +20,7 @@ #include <FindDirectory.h> #include <fs_index.h> #include <IconUtils.h> +#include <MessageFormat.h> #include <NodeMonitor.h> #include <Notification.h> #include <Path.h> @@ -175,17 +176,12 @@ MailDaemonApp::ReadyToRun() fQueries.AddItem(query); } - BString string, numString; + BString string; if (fNewMessages > 0) { - if (fNewMessages != 1) - string << B_TRANSLATE("%num new messages."); - else - string << B_TRANSLATE("%num new message."); - - numString << fNewMessages; - string.ReplaceFirst("%num", numString); - } - else + BMessageFormat().Format(string, B_TRANSLATE( + "{0, plural, one{# new message} other{# new messages}}"), + fNewMessages); + } else string = B_TRANSLATE("No new messages"); fCentralBeep = false; @@ -349,15 +345,10 @@ MailDaemonApp::MessageReceived(BMessage* msg) case 'numg': { int32 numMessages = msg->FindInt32("num_messages"); - BString numString; - - if (numMessages > 1) - fAlertString << B_TRANSLATE("%num new messages for %name\n"); - else - fAlertString << B_TRANSLATE("%num new message for %name\n"); + BMessageFormat().Format(fAlertString, B_TRANSLATE( + "{0, plural, one{# new message} other{# new messages}} " + "for %name\n"), numMessages); - numString << numMessages; - fAlertString.ReplaceFirst("%num", numString); fAlertString.ReplaceFirst("%name", msg->FindString("name")); break; } @@ -375,18 +366,13 @@ MailDaemonApp::MessageReceived(BMessage* msg) break; } - BString string, numString; + BString string; if (fNewMessages > 0) { - if (fNewMessages != 1) - string << B_TRANSLATE("%num new messages."); - else - string << B_TRANSLATE("%num new message."); - - numString << fNewMessages; - string.ReplaceFirst("%num", numString); - } - else + BMessageFormat().Format(string, B_TRANSLATE( + "{0, plural, one{# new message.} other{# new messages.}}"), + fNewMessages); + } else string << B_TRANSLATE("No new messages."); fNotification->SetTitle(string.String());