From 1b5edfc00aa1f862c50665e9561df868d554a221 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Guillot?= Date: Thu, 29 Feb 2024 20:20:29 -0800 Subject: [PATCH] Add unit test to ensure each translation has the correct number of plurals --- internal/locale/catalog_test.go | 23 ++++++++++++-- internal/locale/locale.go | 21 +++++++++++++ internal/locale/plural.go | 21 +++++++++++++ internal/locale/plural_test.go | 28 +++++++++++++++++ internal/locale/translations/id_ID.json | 42 ++++++++++--------------- internal/locale/translations/ja_JP.json | 31 +++++------------- internal/locale/translations/zh_CN.json | 23 +++++--------- internal/locale/translations/zh_TW.json | 33 +++++-------------- 8 files changed, 129 insertions(+), 93 deletions(-) diff --git a/internal/locale/catalog_test.go b/internal/locale/catalog_test.go index 8fb29ddf..b5998ad5 100644 --- a/internal/locale/catalog_test.go +++ b/internal/locale/catalog_test.go @@ -53,11 +53,11 @@ func TestAllKeysHaveValue(t *testing.T) { switch value := v.(type) { case string: if value == "" { - t.Errorf(`The key %q for the language %q have an empty string as value`, k, language) + t.Errorf(`The key %q for the language %q has an empty string as value`, k, language) } - case []string: + case []any: if len(value) == 0 { - t.Errorf(`The key %q for the language %q have an empty list as value`, k, language) + t.Errorf(`The key %q for the language %q has an empty list as value`, k, language) } } } @@ -88,3 +88,20 @@ func TestMissingTranslations(t *testing.T) { } } } + +func TestTranslationFilePluralForms(t *testing.T) { + for language := range AvailableLanguages() { + messages, err := loadTranslationFile(language) + if err != nil { + t.Fatalf(`Unable to load translation messages for language %q`, language) + } + + for k, v := range messages { + if value, ok := v.([]any); ok { + if len(value) != numberOfPluralFormsPerLanguage[language] { + t.Errorf(`The key %q for the language %q does not have the expected number of plurals, got %d instead of %d`, k, language, len(value), numberOfPluralFormsPerLanguage[language]) + } + } + } + } +} diff --git a/internal/locale/locale.go b/internal/locale/locale.go index a5a1010b..acca6707 100644 --- a/internal/locale/locale.go +++ b/internal/locale/locale.go @@ -3,6 +3,27 @@ package locale // import "miniflux.app/v2/internal/locale" +var numberOfPluralFormsPerLanguage = map[string]int{ + "en_US": 2, + "es_ES": 2, + "fr_FR": 2, + "de_DE": 2, + "pl_PL": 3, + "pt_BR": 2, + "zh_CN": 1, + "zh_TW": 1, + "nl_NL": 2, + "ru_RU": 3, + "it_IT": 2, + "ja_JP": 1, + "tr_TR": 2, + "el_EL": 2, + "fi_FI": 2, + "hi_IN": 2, + "uk_UA": 3, + "id_ID": 1, +} + // AvailableLanguages returns the list of available languages. func AvailableLanguages() map[string]string { return map[string]string{ diff --git a/internal/locale/plural.go b/internal/locale/plural.go index ab5d2bbc..8bf6913d 100644 --- a/internal/locale/plural.go +++ b/internal/locale/plural.go @@ -39,10 +39,21 @@ var pluralForms = map[string](func(n int) int){ } return 2 }, + // nplurals=2; plural=(n > 1); + "fr_FR": func(n int) int { + if n > 1 { + return 1 + } + return 0 + }, // nplurals=1; plural=0; "id_ID": func(n int) int { return 0 }, + // nplurals=1; plural=0; + "ja_JP": func(n int) int { + return 0 + }, // nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2); "pl_PL": func(n int) int { switch { @@ -61,12 +72,22 @@ var pluralForms = map[string](func(n int) int){ return 0 }, "ru_RU": pluralFormRuSrUa, + // nplurals=2; plural=(n > 1); + "tr_TR": func(n int) int { + if n > 1 { + return 1 + } + return 0 + }, "uk_UA": pluralFormRuSrUa, "sr_RS": pluralFormRuSrUa, // nplurals=1; plural=0; "zh_CN": func(n int) int { return 0 }, + "zh_TW": func(n int) int { + return 0 + }, } // nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2); diff --git a/internal/locale/plural_test.go b/internal/locale/plural_test.go index 084dd202..2bbf9d50 100644 --- a/internal/locale/plural_test.go +++ b/internal/locale/plural_test.go @@ -25,6 +25,20 @@ func TestPluralRules(t *testing.T) { 2: 1, 5: 2, }, + "fr_FR": { + 1: 0, + 2: 1, + 5: 1, + }, + "id_ID": { + 1: 0, + 5: 0, + }, + "ja_JP": { + 1: 0, + 2: 0, + 5: 0, + }, "pl_PL": { 1: 0, 2: 1, @@ -45,10 +59,24 @@ func TestPluralRules(t *testing.T) { 2: 1, 5: 2, }, + "tr_TR": { + 1: 0, + 2: 1, + 5: 1, + }, + "uk_UA": { + 1: 0, + 2: 1, + 5: 2, + }, "zh_CN": { 1: 0, 5: 0, }, + "zh_TW": { + 1: 0, + 5: 0, + }, } for rule, values := range scenarios { diff --git a/internal/locale/translations/id_ID.json b/internal/locale/translations/id_ID.json index 37a29fee..6453a541 100644 --- a/internal/locale/translations/id_ID.json +++ b/internal/locale/translations/id_ID.json @@ -83,38 +83,33 @@ "entry.shared_entry.title": "Buka tautan publik", "entry.shared_entry.label": "Bagikan", "entry.estimated_reading_time": [ - "%d menit untuk dibaca" + "%d menit untuk dibaca" ], "entry.tags.label": "Tanda:", "page.shared_entries.title": "Entri yang Dibagikan", "page.shared_entries_count": [ - "%d shared entry", - "%d shared entries" + "%d shared entry" ], "page.unread.title": "Belum Dibaca", "page.unread_entry_count": [ - "%d unread entry", - "%d unread entries" + "%d unread entry" ], "page.total_entry_count": [ - "%d entry in total", - "%d entries in total" + "%d entry in total" ], "page.starred.title": "Markah", "page.starred_entry_count": [ - "%d starred entry", - "%d starred entries" + "%d starred entry" ], "page.categories.title": "Kategori", "page.categories.no_feed": "Tidak ada umpan.", "page.categories.entries": "Artikel", "page.categories.feeds": "Langganan", "page.categories.feed_count": [ - "Ada %d umpan." + "Ada %d umpan." ], "page.categories_count": [ - "%d category", - "%d categories" + "%d category" ], "page.new_category.title": "Kategori Baru", "page.new_user.title": "Pengguna Baru", @@ -126,12 +121,11 @@ "page.feeds.next_check": "Next check:", "page.feeds.read_counter": "Jumlah entri yang telah dibaca", "page.feeds.error_count": [ - "%d galat" + "%d galat" ], "page.history.title": "Riwayat", "page.read_entry_count": [ - "%d read entry", - "%d read entries" + "%d read entry" ], "page.import.title": "Impor", "page.search.title": "Hasil Pencarian", @@ -212,8 +206,7 @@ "page.settings.webauthn.register": "Register passkey", "page.settings.webauthn.register.error": "Unable to register passkey", "page.settings.webauthn.delete": [ - "Remove %d passkey", - "Remove %d passkeys" + "Remove %d passkey" ], "page.login.title": "Masuk", "page.login.google_signin": "Masuk dengan Google", @@ -469,26 +462,25 @@ "time_elapsed.yesterday": "kemarin", "time_elapsed.now": "baru saja", "time_elapsed.minutes": [ - "%d menit yang lalu" + "%d menit yang lalu" ], "time_elapsed.hours": [ - "%d jam yang lalu" + "%d jam yang lalu" ], "time_elapsed.days": [ - "%d hari yang lalu" + "%d hari yang lalu" ], "time_elapsed.weeks": [ - "%d pekan yang lalu" + "%d pekan yang lalu" ], "time_elapsed.months": [ - "%d bulan yang lalu" + "%d bulan yang lalu" ], "time_elapsed.years": [ - "%d tahun yang lalu" + "%d tahun yang lalu" ], "alert.too_many_feeds_refresh": [ - "You have triggered too many feed refreshes. Please wait %d minute before trying again.", - "You have triggered too many feed refreshes. Please wait %d minutes before trying again." + "You have triggered too many feed refreshes. Please wait %d minute before trying again." ], "alert.background_feed_refresh": "All feeds are being refreshed in the background. You can continue to use Miniflux while this process is running.", "error.http_response_too_large": "The HTTP response is too large. You could increase the HTTP response size limit in the global settings (requires a server restart).", diff --git a/internal/locale/translations/ja_JP.json b/internal/locale/translations/ja_JP.json index 4f23310b..54bcb3ea 100644 --- a/internal/locale/translations/ja_JP.json +++ b/internal/locale/translations/ja_JP.json @@ -83,40 +83,33 @@ "entry.shared_entry.title": "公開リンクを開く", "entry.shared_entry.label": "共有する", "entry.estimated_reading_time": [ - "%d 分で読めます", "%d 分で読めます" ], "entry.tags.label": "タグ:", "page.shared_entries.title": "共有エントリ", "page.shared_entries_count": [ - "%d shared entry", - "%d shared entries" + "%d shared entry" ], "page.unread.title": "未読", "page.unread_entry_count": [ - "%d unread entry", - "%d unread entries" + "%d unread entry" ], "page.total_entry_count": [ - "%d entry in total", - "%d entries in total" + "%d entry in total" ], "page.starred.title": "星付き", "page.starred_entry_count": [ - "%d starred entry", - "%d starred entries" + "%d starred entry" ], "page.categories.title": "カテゴリ", "page.categories.no_feed": "フィードはありません。", "page.categories.entries": "記事一覧", "page.categories.feeds": "フィード一覧", "page.categories.feed_count": [ - "%d 件のフィードがあります。", "%d 件のフィードがあります。" ], "page.categories_count": [ - "%d category", - "%d categories" + "%d category" ], "page.new_category.title": "新規カテゴリ", "page.new_user.title": "新規ユーザー", @@ -128,13 +121,11 @@ "page.feeds.next_check": "Next check:", "page.feeds.read_counter": "既読記事の数", "page.feeds.error_count": [ - "%d 個のエラー", "%d 個のエラー" ], "page.history.title": "履歴", "page.read_entry_count": [ - "%d read entry", - "%d read entries" + "%d read entry" ], "page.import.title": "インポート", "page.search.title": "検索結果", @@ -215,7 +206,6 @@ "page.settings.webauthn.register": "パスキーを登録する", "page.settings.webauthn.register.error": "パスキーを登録できません", "page.settings.webauthn.delete": [ - "%d 個のパスキーを削除", "%d 個のパスキーを削除" ], "page.login.title": "ログイン", @@ -472,32 +462,25 @@ "time_elapsed.yesterday": "昨日", "time_elapsed.now": "今", "time_elapsed.minutes": [ - "%d 分前", "%d 分前" ], "time_elapsed.hours": [ - "%d 時間前", "%d 時間前" ], "time_elapsed.days": [ - "%d 日前", "%d 日前" ], "time_elapsed.weeks": [ - "%d 週間前", "%d 週間前" ], "time_elapsed.months": [ - "%d か月前", "%d か月前" ], "time_elapsed.years": [ - "%d 年前", "%d 年前" ], "alert.too_many_feeds_refresh": [ - "You have triggered too many feed refreshes. Please wait %d minute before trying again.", - "You have triggered too many feed refreshes. Please wait %d minutes before trying again." + "You have triggered too many feed refreshes. Please wait %d minute before trying again." ], "alert.background_feed_refresh": "All feeds are being refreshed in the background. You can continue to use Miniflux while this process is running.", "error.http_response_too_large": "The HTTP response is too large. You could increase the HTTP response size limit in the global settings (requires a server restart).", diff --git a/internal/locale/translations/zh_CN.json b/internal/locale/translations/zh_CN.json index 9a324b44..13ac7ca1 100644 --- a/internal/locale/translations/zh_CN.json +++ b/internal/locale/translations/zh_CN.json @@ -83,28 +83,23 @@ "entry.shared_entry.title": "打开公共链接", "entry.shared_entry.label": "分享", "entry.estimated_reading_time": [ - "需要 %d 分钟阅读", "需要 %d 分钟阅读" ], "entry.tags.label": "标签:", "page.shared_entries.title": "已分享的文章", "page.shared_entries_count": [ - "%d shared entry", - "%d shared entries" + "%d shared entry" ], "page.unread.title": "未读", "page.unread_entry_count": [ - "%d unread entry", - "%d unread entries" + "%d unread entry" ], "page.total_entry_count": [ - "%d entry in total", - "%d entries in total" + "%d entry in total" ], "page.starred.title": "收藏", "page.starred_entry_count": [ - "%d starred entry", - "%d starred entries" + "%d starred entry" ], "page.categories.title": "分类", "page.categories.no_feed": "没有源", @@ -114,8 +109,7 @@ "有 %d 个源" ], "page.categories_count": [ - "%d category", - "%d categories" + "%d category" ], "page.new_category.title": "新分类", "page.new_user.title": "新用户", @@ -131,8 +125,7 @@ ], "page.history.title": "历史", "page.read_entry_count": [ - "%d read entry", - "%d read entries" + "%d read entry" ], "page.import.title": "导入", "page.search.title": "搜索结果", @@ -213,7 +206,6 @@ "page.settings.webauthn.register": "注册 Passkey", "page.settings.webauthn.register.error": "无法注册 Passkey", "page.settings.webauthn.delete": [ - "删除 %d 个 Passkey", "删除 %d 个 Passkey" ], "page.login.title": "登录", @@ -488,8 +480,7 @@ "%d 年前" ], "alert.too_many_feeds_refresh": [ - "You have triggered too many feed refreshes. Please wait %d minute before trying again.", - "You have triggered too many feed refreshes. Please wait %d minutes before trying again." + "You have triggered too many feed refreshes. Please wait %d minute before trying again." ], "alert.background_feed_refresh": "All feeds are being refreshed in the background. You can continue to use Miniflux while this process is running.", "error.http_response_too_large": "The HTTP response is too large. You could increase the HTTP response size limit in the global settings (requires a server restart).", diff --git a/internal/locale/translations/zh_TW.json b/internal/locale/translations/zh_TW.json index ed0db3a4..4d2e034a 100644 --- a/internal/locale/translations/zh_TW.json +++ b/internal/locale/translations/zh_TW.json @@ -83,40 +83,33 @@ "entry.shared_entry.title": "開啟公共連結", "entry.shared_entry.label": "分享", "entry.estimated_reading_time": [ - "需要 %d 分鐘閱讀", "需要 %d 分鐘閱讀" ], "entry.tags.label": "標籤:", "page.shared_entries.title": "已分享的文章", "page.shared_entries_count": [ - "%d shared entry", - "%d shared entries" + "%d shared entry" ], "page.unread.title": "未讀", "page.unread_entry_count": [ - "%d unread entry", - "%d unread entries" + "%d unread entry" ], "page.total_entry_count": [ - "%d entry in total", - "%d entries in total" + "%d entry in total" ], "page.starred.title": "收藏", "page.starred_entry_count": [ - "%d starred entry", - "%d starred entries" + "%d starred entry" ], "page.categories.title": "分類", "page.categories.no_feed": "沒有Feed", "page.categories.entries": "檢視內容", "page.categories.feeds": "檢視Feeds", "page.categories.feed_count": [ - "有 %d 個Feed", - "有 %d 個Feeds" + "有 %d 個Feed" ], "page.categories_count": [ - "%d category", - "%d categories" + "%d category" ], "page.new_category.title": "新分類", "page.new_user.title": "新使用者", @@ -128,13 +121,11 @@ "page.feeds.next_check": "下次檢查時間:", "page.feeds.read_counter": "已讀文章數", "page.feeds.error_count": [ - "%d 錯誤", "%d 錯誤" ], "page.history.title": "歷史", "page.read_entry_count": [ - "%d read entry", - "%d read entries" + "%d read entry" ], "page.import.title": "匯入", "page.search.title": "搜尋結果", @@ -215,7 +206,6 @@ "page.settings.webauthn.register": "註冊 Passkey", "page.settings.webauthn.register.error": "無法註冊 Passkey", "page.settings.webauthn.delete": [ - "刪除 %d 個 Passkey", "刪除 %d 個 Passkey" ], "page.login.title": "登入", @@ -472,32 +462,25 @@ "time_elapsed.yesterday": "昨天", "time_elapsed.now": "剛剛", "time_elapsed.minutes": [ - "%d 分鐘前", "%d 分鐘前" ], "time_elapsed.hours": [ - "%d 小時前", "%d 小時前" ], "time_elapsed.days": [ - "%d 天前", "%d 天前" ], "time_elapsed.weeks": [ - "%d 周前", "%d 周前" ], "time_elapsed.months": [ - "%d 月前", "%d 月前" ], "time_elapsed.years": [ - "%d 年前", "%d 年前" ], "alert.too_many_feeds_refresh": [ - "You have triggered too many feed refreshes. Please wait %d minute before trying again.", - "You have triggered too many feed refreshes. Please wait %d minutes before trying again." + "You have triggered too many feed refreshes. Please wait %d minute before trying again." ], "alert.background_feed_refresh": "All feeds are being refreshed in the background. You can continue to use Miniflux while this process is running.", "error.http_response_too_large": "The HTTP response is too large. You could increase the HTTP response size limit in the global settings (requires a server restart).",