From 4ea9ae2036957b3c46bd298b51df59f8dc1330fe Mon Sep 17 00:00:00 2001 From: Shlomo <78599753+ShlomoCode@users.noreply.github.com> Date: Thu, 6 Aug 2026 16:38:46 +0300 Subject: [PATCH] i18n: improve Hebrew translations (#3236) --- app/assets/i18n/_missing_translations_he.json | 66 +++++++------------ app/assets/i18n/en.json | 3 + app/assets/i18n/he.json | 25 ++++++- app/lib/gen/strings.g.dart | 2 +- app/lib/gen/strings_en.g.dart | 6 ++ app/lib/gen/strings_he.g.dart | 46 ++++++++++++- app/lib/util/i18n.dart | 3 + app/lib/util/notification_strings.dart | 23 ++++++- .../unit/util/file_speed_helper_he_test.dart | 43 ++++++++++++ .../unit/util/file_speed_helper_test.dart | 10 +-- .../lib/util/file_speed_helper.dart | 4 +- .../lib/util/notification_strings.dart | 14 ++-- 12 files changed, 183 insertions(+), 62 deletions(-) create mode 100644 app/test/unit/util/file_speed_helper_he_test.dart diff --git a/app/assets/i18n/_missing_translations_he.json b/app/assets/i18n/_missing_translations_he.json index 880db29e..fe55033d 100644 --- a/app/assets/i18n/_missing_translations_he.json +++ b/app/assets/i18n/_missing_translations_he.json @@ -4,74 +4,58 @@ "After editing this file, you can run 'dart run slang apply --locale=he' to quickly apply the newly added translations." ], "receiveTab": { - "link": "Receive via link" + "link": "קבלה באמצעות קישור" }, "settingsTab": { "general": { "colorOptions": { - "custom": "Custom" + "custom": "מותאם אישית" } }, "receive": { - "verifyChecksums": "Verify checksums when receiving files" + "verifyChecksums": "אימות שלמות קבצים בעת קבלה" }, "send": { - "createChecksums": "Create checksums when sending files" + "createChecksums": "צור סכומי שלמות קבצים בעת שליחה" } }, "deviceDetailsPage": { - "title": "Device Details", - "favorite": "Favorite", - "verify": "Verify", + "title": "פרטי מכשיר", + "favorite": "מועדף", + "verify": "אימות", "info": { - "name": "Name", - "address": "Address", - "version": "Version", - "protocol": "Protocol v{version}" + "name": "שם", + "address": "כתובת", + "version": "גרסה", + "protocol": "פרוטוקול גרסה {version}" }, "logs": { - "title": "Logs", - "empty": "No logs available.", - "discovered": "Discovered via {protocol} ({host})", - "updated": "Updated via {protocol} ({host})" + "title": "יומני רישום", + "empty": "אין יומנים זמינים", + "discovered": "התגלה באמצעות {protocol} ({host})", + "updated": "עודכן באמצעות {protocol} ({host})" } }, "verifyPage": { - "title": "Verify", - "icons": "Icons", - "raw": "Raw", - "question": "Does it look the same on the other device?" + "title": "אימות", + "icons": "סמלים", + "raw": "גולמי", + "question": "האם זה נראה אותו דבר במכשיר השני?" }, "sendPage": { - "calculatingChecksum": "Calculating checksum ({curr} / {n})" - }, - "progressPage": { - "remainingTime": { - "minutesUnit(plural, param=m)": { - "other": "{m}m" - }, - "hoursUnit(plural, param=h)": { - "other": "{h}h" - }, - "daysUnit(plural, param=d)": { - "other": "{d}d" - }, - "minutes": "{n}:{ss}", - "hours": "@:progressPage.remainingTime.hoursUnit @:progressPage.remainingTime.minutesUnit", - "days": "@:progressPage.remainingTime.daysUnit @:progressPage.remainingTime.hoursUnit @:progressPage.remainingTime.minutesUnit" - } + "calculatingChecksum": "מאמת שלמות ({curr} / {n})" }, "webReceivePage": { - "title": "Receive via link" + "title": "קבלה באמצעות קישור" }, "whatsNewPage": { - "title": "What's new in {version}", + "title": "מה חדש בגרסה {version}?", "changes(interface=WhatsNewStrings)": { "v1_18_0": { "changes": [ - "Encryption no longer slows down transfers. It has been re-enabled on this device if you turned it off before.", - "Requests from favorites are now accepted automatically. This is turned on by default and can be disabled in the settings.", - "On Android, transfers continue while the app is in the background or the screen is off. On iOS, the app must still stay in the foreground." + "הצפנה כבר לא מאטה את ההעברות! הפעלנו אותה מחדש אם כיבית אותה בעבר", + "מהיום העברות ממכשירים מועדפים יתקבלו באופן אוטומטי. ניתן לבטל זאת בהגדרות האפליקציה", + "באנדרואיד, ההעברות נמשכות גם כשהאפליקציה ברקע או שהמסך כבוי (ב-iOS כרגע האפליקציה עדיין חייבת להישאר פתוחה והמסך דולק)" ] } } diff --git a/app/assets/i18n/en.json b/app/assets/i18n/en.json index 8c65b685..0761ee6d 100644 --- a/app/assets/i18n/en.json +++ b/app/assets/i18n/en.json @@ -274,6 +274,9 @@ "hoursUnit(plural, param=h)": { "other": "{h}h" }, + "hoursUnitWithAnd(plural, param=h)": { + "other": "{h}h" + }, "daysUnit(plural, param=d)": { "other": "{d}d" }, diff --git a/app/assets/i18n/he.json b/app/assets/i18n/he.json index f2d87b59..698ca0fa 100644 --- a/app/assets/i18n/he.json +++ b/app/assets/i18n/he.json @@ -196,7 +196,7 @@ "apkPickerPage": { "title": "אפליקציות (APK)", "excludeSystemApps": "אל תכלול אפליקציות מערכת", - "excludeAppsWithoutLaunchIntent": "אל תכלול אפליקציות שאינן ניתנות להפעלה", + "excludeAppsWithoutLaunchIntent": "אל תכלול אפליקציות שאינן ניתנות לפתיחה", "apps": "{n} אפליקציות" }, "selectedFilesPage": { @@ -238,6 +238,29 @@ "count": "קבצים: {curr}/{n}", "size": "גודל: {curr}/{n}", "speed": "מהירות: {speed}/שניה" + }, + "remainingTime": { + "minutesUnit(plural, param=m)": { + "one": "ודקה", + "other": "ו־{m} דקות" + }, + "hoursUnit(plural, param=h)": { + "one": "שעה", + "two": "שעתיים", + "other": "{h} שעות" + }, + "hoursUnitWithAnd(plural, param=h)": { + "one": "ושעה", + "two": "ושעתיים", + "other": "ו־{h} שעות" + }, + "daysUnit(plural, param=d)": { + "one": "יום", + "two": "יומיים", + "other": "{d} ימים" + }, + "hours": "@:progressPage.remainingTime.hoursUnit @:progressPage.remainingTime.minutesUnit", + "days": "@:progressPage.remainingTime.daysUnit, @:progressPage.remainingTime.hoursUnit @:progressPage.remainingTime.minutesUnit" } }, "webSharePage": { diff --git a/app/lib/gen/strings.g.dart b/app/lib/gen/strings.g.dart index c89f175c..54900938 100644 --- a/app/lib/gen/strings.g.dart +++ b/app/lib/gen/strings.g.dart @@ -4,7 +4,7 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 55 -/// Strings: 18438 (335 per locale) +/// Strings: 18452 (335 per locale) // coverage:ignore-file // ignore_for_file: type=lint, unused_import diff --git a/app/lib/gen/strings_en.g.dart b/app/lib/gen/strings_en.g.dart index 00b4886d..0e8bf934 100644 --- a/app/lib/gen/strings_en.g.dart +++ b/app/lib/gen/strings_en.g.dart @@ -1390,6 +1390,12 @@ class Translations$progressPage$remainingTime$en { other: '${h}h', ); + /// en: '(other) {{h}h}' + String hoursUnitWithAnd({required num h}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))( + h, + other: '${h}h', + ); + /// en: '(other) {{d}d}' String daysUnit({required num d}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))( d, diff --git a/app/lib/gen/strings_he.g.dart b/app/lib/gen/strings_he.g.dart index eba5370c..c7507052 100644 --- a/app/lib/gen/strings_he.g.dart +++ b/app/lib/gen/strings_he.g.dart @@ -319,7 +319,7 @@ class _Translations$apkPickerPage$he extends Translations$apkPickerPage$en { @override String get excludeSystemApps => 'אל תכלול אפליקציות מערכת'; @override - String get excludeAppsWithoutLaunchIntent => 'אל תכלול אפליקציות שאינן ניתנות להפעלה'; + String get excludeAppsWithoutLaunchIntent => 'אל תכלול אפליקציות שאינן ניתנות לפתיחה'; @override String apps({required Object n}) => '${n} אפליקציות'; } @@ -407,6 +407,8 @@ class _Translations$progressPage$he extends Translations$progressPage$en { String get savedToGallery => 'נשמר בתמונות'; @override late final _Translations$progressPage$total$he total = _Translations$progressPage$total$he._(_root); + @override + late final _Translations$progressPage$remainingTime$he remainingTime = _Translations$progressPage$remainingTime$he._(_root); } // Path: webSharePage @@ -980,6 +982,48 @@ class _Translations$progressPage$total$he extends Translations$progressPage$tota String speed({required Object speed}) => 'מהירות: ${speed}/שניה'; } +// Path: progressPage.remainingTime +class _Translations$progressPage$remainingTime$he extends Translations$progressPage$remainingTime$en { + _Translations$progressPage$remainingTime$he._(TranslationsHe root) : this._root = root, super.internal(root); + + final TranslationsHe _root; // ignore: unused_field + + // Translations + @override + String minutesUnit({required num m}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('he'))( + m, + one: 'ודקה', + other: 'ו־${m} דקות', + ); + @override + String hoursUnit({required num h}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('he'))( + h, + one: 'שעה', + two: 'שעתיים', + other: '${h} שעות', + ); + @override + String hoursUnitWithAnd({required num h}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('he'))( + h, + one: 'ושעה', + two: 'ושעתיים', + other: 'ו־${h} שעות', + ); + @override + String daysUnit({required num d}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('he'))( + d, + one: 'יום', + two: 'יומיים', + other: '${d} ימים', + ); + @override + String hours({required num h, required num m}) => + '${_root.progressPage.remainingTime.hoursUnit(h: h)} ${_root.progressPage.remainingTime.minutesUnit(m: m)}'; + @override + String days({required num d, required num h, required num m}) => + '${_root.progressPage.remainingTime.daysUnit(d: d)}, ${_root.progressPage.remainingTime.hoursUnit(h: h)} ${_root.progressPage.remainingTime.minutesUnit(m: m)}'; +} + // Path: dialogs.addFile class _Translations$dialogs$addFile$he extends Translations$dialogs$addFile$en { _Translations$dialogs$addFile$he._(TranslationsHe root) : this._root = root, super.internal(root); diff --git a/app/lib/util/i18n.dart b/app/lib/util/i18n.dart index 9f1b5958..9f70546f 100644 --- a/app/lib/util/i18n.dart +++ b/app/lib/util/i18n.dart @@ -16,6 +16,9 @@ Future initI18n() async { if (n == 1) { return one ?? other ?? n.toString(); } + if (locale == AppLocale.he && n == 2) { + return two ?? other ?? n.toString(); + } return other ?? n.toString(); }, ordinalResolver: (n, {zero, one, two, few, many, other}) { diff --git a/app/lib/util/notification_strings.dart b/app/lib/util/notification_strings.dart index d4bdd1dc..11148230 100644 --- a/app/lib/util/notification_strings.dart +++ b/app/lib/util/notification_strings.dart @@ -9,6 +9,25 @@ final notificationStrings = NotificationStrings( titleReceiving: t.progressPage.titleReceiving, titleSending: t.progressPage.titleSending, remainingTimeMinutes: t.progressPage.remainingTime.minutes, - remainingTimeHours: t.progressPage.remainingTime.hours, - remainingTimeDays: t.progressPage.remainingTime.days, + remainingTimeLong: _remainingTimeLong, ); + +String _remainingTimeLong({required num d, required num h, required num m}) { + final remainingTime = t.progressPage.remainingTime; + if (d == 0 && m == 0) { + return remainingTime.hoursUnit(h: h); + } + if (d == 0) { + return remainingTime.hours(h: h, m: m); + } + if (h == 0 && m == 0) { + return remainingTime.daysUnit(d: d); + } + if (h == 0) { + return '${remainingTime.daysUnit(d: d)} ${remainingTime.minutesUnit(m: m)}'; + } + if (m == 0) { + return '${remainingTime.daysUnit(d: d)} ${remainingTime.hoursUnitWithAnd(h: h)}'; + } + return remainingTime.days(d: d, h: h, m: m); +} diff --git a/app/test/unit/util/file_speed_helper_he_test.dart b/app/test/unit/util/file_speed_helper_he_test.dart new file mode 100644 index 00000000..fcb5bd31 --- /dev/null +++ b/app/test/unit/util/file_speed_helper_he_test.dart @@ -0,0 +1,43 @@ +import 'package:localsend_app/gen/strings.g.dart'; +import 'package:localsend_app/util/i18n.dart'; +import 'package:localsend_app/util/notification_strings.dart'; +import 'package:localsend_isolates/util/file_speed_helper.dart'; +import 'package:test/test.dart'; + +void main() { + setUpAll(() async { + await initI18n(); + LocaleSettings.setLocaleSync(AppLocale.he); + }); + + const cases = [ + (Duration(minutes: 10, seconds: 54), '10:54'), + (Duration(hours: 1), 'שעה'), + (Duration(hours: 1, minutes: 1), 'שעה ודקה'), + (Duration(hours: 2, minutes: 2), 'שעתיים ו־2 דקות'), + (Duration(hours: 1, minutes: 5), 'שעה ו־5 דקות'), + (Duration(days: 1), 'יום'), + (Duration(days: 2), 'יומיים'), + (Duration(days: 3), '3 ימים'), + (Duration(days: 1, hours: 1), 'יום ושעה'), + (Duration(days: 1, minutes: 5), 'יום ו־5 דקות'), + ( + Duration(days: 1, hours: 2, minutes: 5), + 'יום, שעתיים ו־5 דקות', + ), + (Duration(hours: 1, minutes: 5, seconds: 9), 'שעה ו־5 דקות'), + ]; + + for (final (duration, expected) in cases) { + test('$duration → $expected', () { + expect( + getRemainingTime( + bytesPerSeconds: 1000, + remainingBytes: duration.inMilliseconds, + strings: notificationStrings, + ), + expected, + ); + }); + } +} diff --git a/app/test/unit/util/file_speed_helper_test.dart b/app/test/unit/util/file_speed_helper_test.dart index 160413eb..2230ae7c 100644 --- a/app/test/unit/util/file_speed_helper_test.dart +++ b/app/test/unit/util/file_speed_helper_test.dart @@ -17,18 +17,18 @@ void main() { }); test('shows hours and minutes for duration 1 hour or more but less than 1 day', () { - expect(getRemainingTime(bytesPerSeconds: 1000, remainingBytes: 3600000, strings: notificationStrings), '1h 0m'); + expect(getRemainingTime(bytesPerSeconds: 1000, remainingBytes: 3600000, strings: notificationStrings), '1h'); expect(getRemainingTime(bytesPerSeconds: 1000, remainingBytes: 3660000, strings: notificationStrings), '1h 1m'); - expect(getRemainingTime(bytesPerSeconds: 1000, remainingBytes: 7200000, strings: notificationStrings), '2h 0m'); + expect(getRemainingTime(bytesPerSeconds: 1000, remainingBytes: 7200000, strings: notificationStrings), '2h'); expect(getRemainingTime(bytesPerSeconds: 1000, remainingBytes: 12000000, strings: notificationStrings), '3h 20m'); expect(getRemainingTime(bytesPerSeconds: 1000, remainingBytes: 86340000, strings: notificationStrings), '23h 59m'); }); test('shows days, hours and minutes for duration 1 day or more', () { - expect(getRemainingTime(bytesPerSeconds: 1000, remainingBytes: 86400000, strings: notificationStrings), '1d 0h 0m'); - expect(getRemainingTime(bytesPerSeconds: 1000, remainingBytes: 90000000, strings: notificationStrings), '1d 1h 0m'); + expect(getRemainingTime(bytesPerSeconds: 1000, remainingBytes: 86400000, strings: notificationStrings), '1d'); + expect(getRemainingTime(bytesPerSeconds: 1000, remainingBytes: 90000000, strings: notificationStrings), '1d 1h'); expect(getRemainingTime(bytesPerSeconds: 1000, remainingBytes: 93660000, strings: notificationStrings), '1d 2h 1m'); - expect(getRemainingTime(bytesPerSeconds: 1000, remainingBytes: 172800000, strings: notificationStrings), '2d 0h 0m'); + expect(getRemainingTime(bytesPerSeconds: 1000, remainingBytes: 172800000, strings: notificationStrings), '2d'); expect(getRemainingTime(bytesPerSeconds: 1000, remainingBytes: 183780000, strings: notificationStrings), '2d 3h 3m'); }); diff --git a/packages/localsend_isolates/lib/util/file_speed_helper.dart b/packages/localsend_isolates/lib/util/file_speed_helper.dart index c1c52fb8..2cae263a 100644 --- a/packages/localsend_isolates/lib/util/file_speed_helper.dart +++ b/packages/localsend_isolates/lib/util/file_speed_helper.dart @@ -32,13 +32,13 @@ String getRemainingTime({ } else if (remainingTimeInSeconds < _secondsPerDay) { final hours = remainingTimeInSeconds ~/ _secondsPerHour; final minutes = (remainingTimeInSeconds % _secondsPerHour) ~/ _secondsPerMinute; - return strings.remainingTimeHours(h: hours, m: minutes); + return strings.remainingTimeLong(d: 0, h: hours, m: minutes); } else { final days = remainingTimeInSeconds ~/ _secondsPerDay; final remainingAfterDays = remainingTimeInSeconds % _secondsPerDay; final hours = remainingAfterDays ~/ _secondsPerHour; final minutes = (remainingAfterDays % _secondsPerHour) ~/ _secondsPerMinute; - return strings.remainingTimeDays(d: days, h: hours, m: minutes); + return strings.remainingTimeLong(d: days, h: hours, m: minutes); } } diff --git a/packages/localsend_isolates/lib/util/notification_strings.dart b/packages/localsend_isolates/lib/util/notification_strings.dart index 31ce25a8..6d2398e5 100644 --- a/packages/localsend_isolates/lib/util/notification_strings.dart +++ b/packages/localsend_isolates/lib/util/notification_strings.dart @@ -1,8 +1,8 @@ /// The translated strings that this package needs but cannot produce on its own: the app owns the /// translations, and the dependency only points from the app to this package. /// -/// Inject an instance via `TransferNotification.init`. The signatures mirror the ones slang -/// generates, so the app can hand over its translation members directly. +/// Inject an instance via `TransferNotification.init` to keep this package independent of the +/// app's localization implementation. class NotificationStrings { /// Title while files are being received, e.g. "Receiving files". final String titleReceiving; @@ -13,17 +13,13 @@ class NotificationStrings { /// Remaining time below an hour, e.g. "1:30" or "0:45". [ss] is zero padded. final String Function({required Object n, required Object ss}) remainingTimeMinutes; - /// Remaining time below a day, e.g. "2h 5m". - final String Function({required num h, required num m}) remainingTimeHours; - - /// Remaining time of a day or more, e.g. "3d 4h 5m". - final String Function({required num d, required num h, required num m}) remainingTimeDays; + /// Remaining time of an hour or more, e.g. "2h 5m" or "3d 4h 5m". + final String Function({required num d, required num h, required num m}) remainingTimeLong; const NotificationStrings({ required this.titleReceiving, required this.titleSending, required this.remainingTimeMinutes, - required this.remainingTimeHours, - required this.remainingTimeDays, + required this.remainingTimeLong, }); }