diff options
-rwxr-xr-x | po/compile.py | 48 | ||||
-rw-r--r-- | po/en.po | 3 | ||||
-rw-r--r-- | res/lang/de.bin | bin | 865 -> 3068 bytes | |||
-rw-r--r-- | res/lang/en.bin | bin | 15248 -> 15487 bytes | |||
-rw-r--r-- | res/lang/fi.bin | bin | 16677 -> 16959 bytes | |||
-rw-r--r-- | res/lang/ru.bin | bin | 20312 -> 23240 bytes | |||
-rw-r--r-- | src/app.c | 3 | ||||
-rw-r--r-- | src/lang.c | 69 | ||||
-rw-r--r-- | src/lang.h | 4 | ||||
-rw-r--r-- | src/ui/documentwidget.c | 29 | ||||
-rw-r--r-- | src/ui/sidebarwidget.c | 8 |
11 files changed, 124 insertions, 40 deletions
diff --git a/po/compile.py b/po/compile.py index cfe837ea..6416ea0e 100755 --- a/po/compile.py +++ b/po/compile.py | |||
@@ -38,29 +38,46 @@ def unquote(string): | |||
38 | 38 | ||
39 | def parse_po(src): | 39 | def parse_po(src): |
40 | messages = [] | 40 | messages = [] |
41 | is_multi = False | 41 | is_multi = False # string is multiple lines |
42 | msg_id, msg_str = None, None | 42 | is_plural = False |
43 | msg_id, msg_str, msg_index = None, None, None | ||
43 | for line in open(src, 'rt', encoding='utf-8').readlines(): | 44 | for line in open(src, 'rt', encoding='utf-8').readlines(): |
44 | line = line.strip() | 45 | line = line.strip() |
45 | if is_multi: | 46 | if is_multi: |
46 | if len(line) == 0: | 47 | if len(line) == 0 or line[0] != '"': |
47 | if msg_id: | 48 | if msg_id: |
48 | messages.append((msg_id, msg_str)) | 49 | messages.append((msg_id, msg_str, msg_index)) |
49 | is_multi = False | 50 | is_multi = False |
50 | continue | ||
51 | else: | 51 | else: |
52 | msg_str += unquote(line) | 52 | msg_str += unquote(line) |
53 | if line.startswith('msgid'): | 53 | if line.startswith('msgid_plural'): |
54 | msg_id = unquote(line[12:]) | ||
55 | is_plural = True | ||
56 | elif line.startswith('msgid'): | ||
54 | msg_id = unquote(line[6:]) | 57 | msg_id = unquote(line[6:]) |
55 | elif line == 'msgstr ""': | 58 | is_plural = False |
56 | # Multiline string. | 59 | elif line.startswith('msgstr'): |
57 | is_multi = True | 60 | if line[6] == '[': |
58 | msg_str = '' | 61 | msg_index = int(line[7]) |
59 | elif line.startswith('msgstr'): | 62 | line = line[9:] |
60 | msg_str = unquote(line[7:]) | 63 | else: |
61 | if msg_id: | 64 | msg_index = None |
62 | messages.append((msg_id, msg_str)) | 65 | line = line[6:] |
63 | return messages | 66 | if line.endswith(' ""'): |
67 | is_multi = True | ||
68 | msg_str = '' | ||
69 | else: | ||
70 | msg_str = unquote(line) | ||
71 | if msg_id: | ||
72 | messages.append((msg_id, msg_str, msg_index)) | ||
73 | # Apply plural indices to ids. | ||
74 | pluralized = [] | ||
75 | for msg_id, msg_str, msg_index in messages: | ||
76 | if not msg_index is None: | ||
77 | msg_id = f'{msg_id[:-1]}{msg_index}' | ||
78 | pluralized.append((msg_id, msg_str)) | ||
79 | #print(msg_id, '=>', msg_str) | ||
80 | return pluralized | ||
64 | 81 | ||
65 | 82 | ||
66 | if MODE == 'compile': | 83 | if MODE == 'compile': |
@@ -76,6 +93,7 @@ if MODE == 'compile': | |||
76 | elif MODE == 'new': | 93 | elif MODE == 'new': |
77 | messages = parse_po('en.po') | 94 | messages = parse_po('en.po') |
78 | f = open('new.po', 'wt', encoding='utf-8') | 95 | f = open('new.po', 'wt', encoding='utf-8') |
96 | # TODO: plurals | ||
79 | for msg_id, _ in messages: | 97 | for msg_id, _ in messages: |
80 | print(f'\nmsgid "{msg_id}"\nmsgstr ""\n', file=f) | 98 | print(f'\nmsgid "{msg_id}"\nmsgstr ""\n', file=f) |
81 | 99 | ||
@@ -1,3 +1,4 @@ | |||
1 | # Alt-text of the preformatted logo. | ||
1 | msgid "about.logo" | 2 | msgid "about.logo" |
2 | msgstr "ASCII art: the word \"Lagrange\" using a large font" | 3 | msgstr "ASCII art: the word \"Lagrange\" using a large font" |
3 | 4 | ||
@@ -73,9 +74,11 @@ msgstr "Find on Page" | |||
73 | msgid "macos.menu.find" | 74 | msgid "macos.menu.find" |
74 | msgstr "Find" | 75 | msgstr "Find" |
75 | 76 | ||
77 | # Used on iOS. "Files" refers to Apple's iOS app where you can pick an iCloud folder. | ||
76 | msgid "menu.save.files" | 78 | msgid "menu.save.files" |
77 | msgstr "Save to Files" | 79 | msgstr "Save to Files" |
78 | 80 | ||
81 | # Used on desktop operating systems. "Downloads" refers to the user's configured downloads directory. | ||
79 | msgid "menu.save.downloads" | 82 | msgid "menu.save.downloads" |
80 | msgstr "Save to Downloads" | 83 | msgstr "Save to Downloads" |
81 | 84 | ||
diff --git a/res/lang/de.bin b/res/lang/de.bin index 4b959add..9a8e509f 100644 --- a/res/lang/de.bin +++ b/res/lang/de.bin | |||
Binary files differ | |||
diff --git a/res/lang/en.bin b/res/lang/en.bin index f8e3480e..427ce70b 100644 --- a/res/lang/en.bin +++ b/res/lang/en.bin | |||
Binary files differ | |||
diff --git a/res/lang/fi.bin b/res/lang/fi.bin index ba6ab16e..b92470d0 100644 --- a/res/lang/fi.bin +++ b/res/lang/fi.bin | |||
Binary files differ | |||
diff --git a/res/lang/ru.bin b/res/lang/ru.bin index 67f5878e..8b75ebce 100644 --- a/res/lang/ru.bin +++ b/res/lang/ru.bin | |||
Binary files differ | |||
@@ -756,7 +756,8 @@ const iString *downloadPathForUrl_App(const iString *url, const iString *mime) { | |||
756 | } | 756 | } |
757 | } | 757 | } |
758 | else { | 758 | else { |
759 | iRangecc fn = { parts.path.start + lastIndexOfCStr_Rangecc(parts.path, "/") + 1, | 759 | const size_t slashPos = lastIndexOfCStr_Rangecc(parts.path, "/"); |
760 | iRangecc fn = { parts.path.start + (slashPos != iInvalidPos ? slashPos + 1 : 0), | ||
760 | parts.path.end }; | 761 | parts.path.end }; |
761 | if (!isEmpty_Range(&fn)) { | 762 | if (!isEmpty_Range(&fn)) { |
762 | setRange_String(name, fn); | 763 | setRange_String(name, fn); |
@@ -19,12 +19,32 @@ int cmp_MsgStr_(const void *e1, const void *e2) { | |||
19 | 19 | ||
20 | /*----------------------------------------------------------------------------------------------*/ | 20 | /*----------------------------------------------------------------------------------------------*/ |
21 | 21 | ||
22 | enum iPluralType { | ||
23 | none_PluralType, | ||
24 | notEqualToOne_PluralType, | ||
25 | slavic_PluralType, | ||
26 | }; | ||
27 | |||
22 | struct Impl_Lang { | 28 | struct Impl_Lang { |
23 | iSortedArray *messages; | 29 | iSortedArray *messages; |
30 | enum iPluralType pluralType; | ||
24 | }; | 31 | }; |
25 | 32 | ||
26 | static iLang lang_; | 33 | static iLang lang_; |
27 | 34 | ||
35 | static size_t pluralIndex_Lang_(const iLang *d, int n) { | ||
36 | switch (d->pluralType) { | ||
37 | case notEqualToOne_PluralType: | ||
38 | return n != 1; | ||
39 | case slavic_PluralType: | ||
40 | return n % 10 == 1 && n % 100 != 11 ? 0 | ||
41 | : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 | ||
42 | : 2; | ||
43 | default: | ||
44 | return 0; | ||
45 | } | ||
46 | } | ||
47 | |||
28 | static void clear_Lang_(iLang *d) { | 48 | static void clear_Lang_(iLang *d) { |
29 | clear_SortedArray(d->messages); | 49 | clear_SortedArray(d->messages); |
30 | } | 50 | } |
@@ -36,15 +56,30 @@ static void load_Lang_(iLang *d, const char *id) { | |||
36 | : equal_CStr(id, "ru") ? &blobRu_Embedded | 56 | : equal_CStr(id, "ru") ? &blobRu_Embedded |
37 | : equal_CStr(id, "de") ? &blobDe_Embedded | 57 | : equal_CStr(id, "de") ? &blobDe_Embedded |
38 | : &blobEn_Embedded; | 58 | : &blobEn_Embedded; |
59 | if (data == &blobRu_Embedded) { | ||
60 | d->pluralType = slavic_PluralType; | ||
61 | } | ||
62 | // else if (data == &blobZhHans_Embedded) { | ||
63 | // d->pluralType = none_PluralType; | ||
64 | // } | ||
65 | else { | ||
66 | d->pluralType = notEqualToOne_PluralType; | ||
67 | } | ||
39 | iMsgStr msg; | 68 | iMsgStr msg; |
40 | for (const char *ptr = constBegin_Block(data); ptr != constEnd_Block(data); ptr++) { | 69 | for (const char *ptr = constBegin_Block(data); ptr != constEnd_Block(data); ptr++) { |
41 | msg.id.start = ptr; | 70 | msg.id.start = ptr; |
42 | while (*++ptr) {} | 71 | while (*++ptr) {} |
43 | msg.id.end = ptr; | 72 | msg.id.end = ptr; |
44 | msg.str.start = ++ptr; | 73 | msg.str.start = ++ptr; |
45 | while (*++ptr) {} | 74 | if (*ptr) { /* not empty */ |
46 | msg.str.end = ptr; | 75 | while (*++ptr) {} |
76 | msg.str.end = ptr; | ||
77 | } | ||
78 | else { | ||
79 | msg.str = msg.id; /* not translated */ | ||
80 | } | ||
47 | /* Allocate the string. The data has already been sorted. */ | 81 | /* Allocate the string. The data has already been sorted. */ |
82 | printf("ID:%s\n", msg.id.start); | ||
48 | pushBack_Array(&d->messages->values, &msg); | 83 | pushBack_Array(&d->messages->values, &msg); |
49 | } | 84 | } |
50 | } | 85 | } |
@@ -87,12 +122,30 @@ iRangecc range_Lang(iRangecc msgId) { | |||
87 | return str; | 122 | return str; |
88 | } | 123 | } |
89 | 124 | ||
125 | const iString *string_Lang(const char *msgId) { | ||
126 | return collectNewRange_String(range_Lang(range_CStr(msgId))); | ||
127 | } | ||
128 | |||
90 | const char *cstr_Lang(const char *msgId) { | 129 | const char *cstr_Lang(const char *msgId) { |
91 | return range_Lang(range_CStr(msgId)).start; /* guaranteed to be NULL-terminated */ | 130 | return range_Lang(range_CStr(msgId)).start; /* guaranteed to be NULL-terminated */ |
92 | } | 131 | } |
93 | 132 | ||
94 | const iString *string_Lang(const char *msgId) { | 133 | static char *pluralId_Lang_(const iLang *d, const char *msgId, int count) { |
95 | return collectNewRange_String(range_Lang(range_CStr(msgId))); | 134 | const size_t len = strlen(msgId); |
135 | char *pluralId = strdup(msgId); | ||
136 | pluralId[len - 1] = '0' + pluralIndex_Lang_(d, count); | ||
137 | return pluralId; | ||
138 | } | ||
139 | |||
140 | const char *cstrCount_Lang(const char *msgId, int count) { | ||
141 | iAssert(endsWith_Rangecc(range_CStr(msgId), ".n")); /* by convention */ | ||
142 | char *pluralId = pluralId_Lang_(&lang_, msgId, count); | ||
143 | const char *str = cstr_Lang(pluralId); | ||
144 | if (str == pluralId) { | ||
145 | str = msgId; /* not found */ | ||
146 | } | ||
147 | free(pluralId); | ||
148 | return str; | ||
96 | } | 149 | } |
97 | 150 | ||
98 | void translate_Lang(iString *textWithIds) { | 151 | void translate_Lang(iString *textWithIds) { |
@@ -129,3 +182,11 @@ const char *translateCStr_Lang(const char *textWithIds) { | |||
129 | translate_Lang(text); | 182 | translate_Lang(text); |
130 | return cstr_String(text); | 183 | return cstr_String(text); |
131 | } | 184 | } |
185 | |||
186 | const char *formatCStr_Lang(const char *formatMsgId, int count) { | ||
187 | return format_CStr(cstrCount_Lang(formatMsgId, count), count); | ||
188 | } | ||
189 | |||
190 | const char *formatCStrs_Lang(const char *formatMsgId, size_t count) { | ||
191 | return format_CStr(cstrCount_Lang(formatMsgId, (int) count), count); | ||
192 | } | ||
@@ -13,3 +13,7 @@ const iString * string_Lang (const char *msgId); | |||
13 | 13 | ||
14 | void translate_Lang (iString *textWithIds); | 14 | void translate_Lang (iString *textWithIds); |
15 | const char * translateCStr_Lang (const char *textWithIds); | 15 | const char * translateCStr_Lang (const char *textWithIds); |
16 | |||
17 | const char * cstrCount_Lang (const char *msgId, int count); | ||
18 | const char * formatCStr_Lang (const char *formatMsgId, int count); | ||
19 | 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) | |||
2081 | } | 2081 | } |
2082 | if (!isEmpty_PtrArray(links)) { | 2082 | if (!isEmpty_PtrArray(links)) { |
2083 | if (argLabel_Command(cmd, "confirm")) { | 2083 | if (argLabel_Command(cmd, "confirm")) { |
2084 | //const char *plural = size_PtrArray(links) != 1 ? "s" : ""; | 2084 | const size_t count = size_PtrArray(links); |
2085 | const iBool isPlural = size_PtrArray(links) != 1; | ||
2086 | makeQuestion_Widget( | 2085 | makeQuestion_Widget( |
2087 | uiHeading_ColorEscape "${heading.import.bookmarks}", | 2086 | uiHeading_ColorEscape "${heading.import.bookmarks}", |
2088 | format_CStr(cstr_Lang(isPlural ? "dlg.import.found.many" : "dlg.import.found"), | 2087 | formatCStrs_Lang("dlg.import.found.n", count), |
2089 | size_PtrArray(links)), | 2088 | (iMenuItem[]){ { "${cancel}", 0, 0, NULL }, |
2090 | (iMenuItem[]){ | 2089 | { format_CStr(cstrCount_Lang("dlg.import.add.n", count), |
2091 | { "${cancel}", 0, 0, NULL }, | 2090 | uiTextAction_ColorEscape, |
2092 | { format_CStr(cstr_Lang(isPlural ? "dlg.import.add.many" : "dlg.import.add"), | 2091 | count), |
2093 | uiTextAction_ColorEscape, | 2092 | 0, |
2094 | size_PtrArray(links)), | 2093 | 0, |
2095 | 0, | 2094 | "bookmark.links" } }, |
2096 | 0, | ||
2097 | "bookmark.links" } }, | ||
2098 | 2); | 2095 | 2); |
2099 | } | 2096 | } |
2100 | else { | 2097 | else { |
@@ -2846,10 +2843,10 @@ static void drawBannerRun_DrawContext_(iDrawContext *d, const iGmRun *run, iInt2 | |||
2846 | const int days = secondsSince_Time(&oldUntil, &now) / 3600 / 24; | 2843 | const int days = secondsSince_Time(&oldUntil, &now) / 3600 / 24; |
2847 | appendCStr_String(&str, "\n"); | 2844 | appendCStr_String(&str, "\n"); |
2848 | if (days <= 30) { | 2845 | if (days <= 30) { |
2849 | appendFormat_String(&str, | 2846 | appendCStr_String(&str, |
2850 | cstr_Lang("dlg.certwarn.mayberenewed"), | 2847 | format_CStr(cstrCount_Lang("dlg.certwarn.mayberenewed.n", days), |
2851 | cstrCollect_String(format_Date(&exp, "%Y-%m-%d")), | 2848 | cstrCollect_String(format_Date(&exp, "%Y-%m-%d")), |
2852 | days); | 2849 | days)); |
2853 | } | 2850 | } |
2854 | else { | 2851 | else { |
2855 | appendCStr_String(&str, cstr_Lang("dlg.certwarn.different")); | 2852 | 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) { | |||
352 | "%s", | 352 | "%s", |
353 | isActive ? cstr_Lang("ident.using") | 353 | isActive ? cstr_Lang("ident.using") |
354 | : isUsed_GmIdentity(ident) | 354 | : isUsed_GmIdentity(ident) |
355 | ? format_CStr(cstr_Lang("ident.usedonurls"), size_StringSet(ident->useUrls)) | 355 | ? formatCStrs_Lang("ident.usedonurls.n", size_StringSet(ident->useUrls)) |
356 | : cstr_Lang("ident.notused")); | 356 | : cstr_Lang("ident.notused")); |
357 | const char *expiry = | 357 | const char *expiry = |
358 | ident->flags & temporary_GmIdentityFlag | 358 | ident->flags & temporary_GmIdentityFlag |
@@ -705,12 +705,12 @@ static void checkModeButtonLayout_SidebarWidget_(iSidebarWidget *d) { | |||
705 | if (i == feeds_SidebarMode && d->numUnreadEntries) { | 705 | if (i == feeds_SidebarMode && d->numUnreadEntries) { |
706 | updateText_LabelWidget( | 706 | updateText_LabelWidget( |
707 | button, | 707 | button, |
708 | collectNewFormat_String("%s " uiTextAction_ColorEscape "%zu%s", | 708 | collectNewFormat_String("%s " uiTextAction_ColorEscape "%zu%s%s", |
709 | tightModeLabels_[i], | 709 | tightModeLabels_[i], |
710 | d->numUnreadEntries, | 710 | d->numUnreadEntries, |
711 | !isTight ? " " : "", | ||
711 | !isTight | 712 | !isTight |
712 | ? (d->numUnreadEntries == 1 ? " ${sidebar.unread}" | 713 | ? formatCStrs_Lang("sidebar.unread.n", d->numUnreadEntries) |
713 | : " ${sidebar.unread.many}") | ||
714 | : "")); | 714 | : "")); |
715 | } | 715 | } |
716 | else { | 716 | else { |