From e8c65a9cc9aee8f49df01e0452516adb1eb7f289 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Thu, 25 Mar 2021 08:48:51 +0200 Subject: Lang: Plural strings IssueID #192 --- po/compile.py | 48 ++++++++++++++++++++++++++------------ po/en.po | 3 +++ res/lang/en.bin | Bin 15248 -> 15487 bytes res/lang/fi.bin | Bin 16677 -> 14726 bytes src/lang.c | 60 ++++++++++++++++++++++++++++++++++++++++++++++-- src/lang.h | 4 ++++ src/ui/documentwidget.c | 29 +++++++++++------------ src/ui/sidebarwidget.c | 8 +++---- 8 files changed, 115 insertions(+), 37 deletions(-) diff --git a/po/compile.py b/po/compile.py index cfe837ea..0a0eab91 100755 --- a/po/compile.py +++ b/po/compile.py @@ -38,29 +38,46 @@ def unquote(string): def parse_po(src): messages = [] - is_multi = False - msg_id, msg_str = None, None + is_multi = False # string is multiple lines + is_plural = False + msg_id, msg_str, msg_index = None, None, None for line in open(src, 'rt', encoding='utf-8').readlines(): line = line.strip() if is_multi: - if len(line) == 0: + if len(line) == 0 or line[0] != '"': if msg_id: - messages.append((msg_id, msg_str)) + messages.append((msg_id, msg_str, msg_index)) is_multi = False - continue + if len(line) == 0: continue else: msg_str += unquote(line) - if line.startswith('msgid'): + if line.startswith('msgid_plural'): + msg_id = unquote(line[12:]) + is_plural = True + elif line.startswith('msgid'): msg_id = unquote(line[6:]) - elif line == 'msgstr ""': - # Multiline string. - is_multi = True - msg_str = '' - elif line.startswith('msgstr'): - msg_str = unquote(line[7:]) - if msg_id: - messages.append((msg_id, msg_str)) - return messages + is_plural = False + elif line.startswith('msgstr'): + if line[6] == '[': + msg_index = int(line[7]) + line = line[9:] + else: + msg_index = None + line = line[7:] + if line.endswith(' ""'): + is_multi = True + msg_str = '' + else: + msg_str = unquote(line) + if msg_id: + messages.append((msg_id, msg_str, msg_index)) + # Apply plural indices to ids. + pluralized = [] + for msg_id, msg_str, msg_index in messages: + if not msg_index is None: + msg_id = f'{msg_id[:-1]}{msg_index}' + pluralized.append((msg_id, msg_str)) + return pluralized if MODE == 'compile': @@ -76,6 +93,7 @@ if MODE == 'compile': elif MODE == 'new': messages = parse_po('en.po') f = open('new.po', 'wt', encoding='utf-8') + # TODO: plurals for msg_id, _ in messages: print(f'\nmsgid "{msg_id}"\nmsgstr ""\n', file=f) diff --git a/po/en.po b/po/en.po index 36f30eda..ad7864b3 100644 --- a/po/en.po +++ b/po/en.po @@ -1,3 +1,4 @@ +# Alt-text of the preformatted logo. msgid "about.logo" msgstr "ASCII art: the word \"Lagrange\" using a large font" @@ -73,9 +74,11 @@ msgstr "Find on Page" msgid "macos.menu.find" msgstr "Find" +# Used on iOS. "Files" refers to Apple's iOS app where you can pick an iCloud folder. msgid "menu.save.files" msgstr "Save to Files" +# Used on desktop operating systems. "Downloads" refers to the user's configured downloads directory. msgid "menu.save.downloads" msgstr "Save to Downloads" diff --git a/res/lang/en.bin b/res/lang/en.bin index f8e3480e..427ce70b 100644 Binary files a/res/lang/en.bin and b/res/lang/en.bin differ diff --git a/res/lang/fi.bin b/res/lang/fi.bin index ba6ab16e..89af1575 100644 Binary files a/res/lang/fi.bin and b/res/lang/fi.bin differ diff --git a/src/lang.c b/src/lang.c index be8ad9d1..983ae3c5 100644 --- a/src/lang.c +++ b/src/lang.c @@ -19,12 +19,32 @@ int cmp_MsgStr_(const void *e1, const void *e2) { /*----------------------------------------------------------------------------------------------*/ +enum iPluralType { + none_PluralType, + notEqualToOne_PluralType, + slavic_PluralType, +}; + struct Impl_Lang { iSortedArray *messages; + enum iPluralType pluralType; }; static iLang lang_; +static size_t pluralIndex_Lang_(const iLang *d, int n) { + switch (d->pluralType) { + case notEqualToOne_PluralType: + return n != 1; + case slavic_PluralType: + return n % 10 == 1 && n % 100 != 11 ? 0 + : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 + : 2; + default: + return 0; + } +} + static void clear_Lang_(iLang *d) { clear_SortedArray(d->messages); } @@ -36,6 +56,15 @@ static void load_Lang_(iLang *d, const char *id) { : equal_CStr(id, "ru") ? &blobRu_Embedded : equal_CStr(id, "de") ? &blobDe_Embedded : &blobEn_Embedded; + if (data == &blobRu_Embedded) { + d->pluralType = slavic_PluralType; + } +// else if (data == &blobZhHans_Embedded) { +// d->pluralType = none_PluralType; +// } + else { + d->pluralType = notEqualToOne_PluralType; + } iMsgStr msg; for (const char *ptr = constBegin_Block(data); ptr != constEnd_Block(data); ptr++) { msg.id.start = ptr; @@ -45,6 +74,7 @@ static void load_Lang_(iLang *d, const char *id) { while (*++ptr) {} msg.str.end = ptr; /* Allocate the string. The data has already been sorted. */ + printf("ID:%s\n", msg.id.start); pushBack_Array(&d->messages->values, &msg); } } @@ -87,12 +117,30 @@ iRangecc range_Lang(iRangecc msgId) { return str; } +const iString *string_Lang(const char *msgId) { + return collectNewRange_String(range_Lang(range_CStr(msgId))); +} + const char *cstr_Lang(const char *msgId) { return range_Lang(range_CStr(msgId)).start; /* guaranteed to be NULL-terminated */ } -const iString *string_Lang(const char *msgId) { - return collectNewRange_String(range_Lang(range_CStr(msgId))); +static char *pluralId_Lang_(const iLang *d, const char *msgId, int count) { + const size_t len = strlen(msgId); + char *pluralId = strdup(msgId); + pluralId[len - 1] = '0' + pluralIndex_Lang_(d, count); + return pluralId; +} + +const char *cstrCount_Lang(const char *msgId, int count) { + iAssert(endsWith_Rangecc(range_CStr(msgId), ".n")); /* by convention */ + char *pluralId = pluralId_Lang_(&lang_, msgId, count); + const char *str = cstr_Lang(pluralId); + if (str == pluralId) { + str = msgId; /* not found */ + } + free(pluralId); + return str; } void translate_Lang(iString *textWithIds) { @@ -129,3 +177,11 @@ const char *translateCStr_Lang(const char *textWithIds) { translate_Lang(text); return cstr_String(text); } + +const char *formatCStr_Lang(const char *formatMsgId, int count) { + return format_CStr(cstrCount_Lang(formatMsgId, count), count); +} + +const char *formatCStrs_Lang(const char *formatMsgId, size_t count) { + return format_CStr(cstrCount_Lang(formatMsgId, (int) count), count); +} diff --git a/src/lang.h b/src/lang.h index ea71e531..bf4c16d1 100644 --- a/src/lang.h +++ b/src/lang.h @@ -13,3 +13,7 @@ const iString * string_Lang (const char *msgId); void translate_Lang (iString *textWithIds); const char * translateCStr_Lang (const char *textWithIds); + +const char * cstrCount_Lang (const char *msgId, int count); +const char * formatCStr_Lang (const char *formatMsgId, int count); +const char * formatCStrs_Lang (const char *formatMsgId, size_t count); diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index 3d8d4f12..88016b1c 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -2081,20 +2081,17 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) } if (!isEmpty_PtrArray(links)) { if (argLabel_Command(cmd, "confirm")) { - //const char *plural = size_PtrArray(links) != 1 ? "s" : ""; - const iBool isPlural = size_PtrArray(links) != 1; + const size_t count = size_PtrArray(links); makeQuestion_Widget( uiHeading_ColorEscape "${heading.import.bookmarks}", - format_CStr(cstr_Lang(isPlural ? "dlg.import.found.many" : "dlg.import.found"), - size_PtrArray(links)), - (iMenuItem[]){ - { "${cancel}", 0, 0, NULL }, - { format_CStr(cstr_Lang(isPlural ? "dlg.import.add.many" : "dlg.import.add"), - uiTextAction_ColorEscape, - size_PtrArray(links)), - 0, - 0, - "bookmark.links" } }, + formatCStrs_Lang("dlg.import.found.n", count), + (iMenuItem[]){ { "${cancel}", 0, 0, NULL }, + { format_CStr(cstrCount_Lang("dlg.import.add.n", count), + uiTextAction_ColorEscape, + count), + 0, + 0, + "bookmark.links" } }, 2); } else { @@ -2846,10 +2843,10 @@ static void drawBannerRun_DrawContext_(iDrawContext *d, const iGmRun *run, iInt2 const int days = secondsSince_Time(&oldUntil, &now) / 3600 / 24; appendCStr_String(&str, "\n"); if (days <= 30) { - appendFormat_String(&str, - cstr_Lang("dlg.certwarn.mayberenewed"), - cstrCollect_String(format_Date(&exp, "%Y-%m-%d")), - days); + appendCStr_String(&str, + format_CStr(cstrCount_Lang("dlg.certwarn.mayberenewed.n", days), + cstrCollect_String(format_Date(&exp, "%Y-%m-%d")), + days)); } else { appendCStr_String(&str, cstr_Lang("dlg.certwarn.different")); diff --git a/src/ui/sidebarwidget.c b/src/ui/sidebarwidget.c index 5ff585a5..29b3646a 100644 --- a/src/ui/sidebarwidget.c +++ b/src/ui/sidebarwidget.c @@ -352,7 +352,7 @@ static void updateItems_SidebarWidget_(iSidebarWidget *d) { "%s", isActive ? cstr_Lang("ident.using") : isUsed_GmIdentity(ident) - ? format_CStr(cstr_Lang("ident.usedonurls"), size_StringSet(ident->useUrls)) + ? formatCStrs_Lang("ident.usedonurls.n", size_StringSet(ident->useUrls)) : cstr_Lang("ident.notused")); const char *expiry = ident->flags & temporary_GmIdentityFlag @@ -705,12 +705,12 @@ static void checkModeButtonLayout_SidebarWidget_(iSidebarWidget *d) { if (i == feeds_SidebarMode && d->numUnreadEntries) { updateText_LabelWidget( button, - collectNewFormat_String("%s " uiTextAction_ColorEscape "%zu%s", + collectNewFormat_String("%s " uiTextAction_ColorEscape "%zu%s%s", tightModeLabels_[i], d->numUnreadEntries, + !isTight ? " " : "", !isTight - ? (d->numUnreadEntries == 1 ? " ${sidebar.unread}" - : " ${sidebar.unread.many}") + ? formatCStrs_Lang("sidebar.unread.n", d->numUnreadEntries) : "")); } else { -- cgit v1.2.3