i18n: improve Hebrew translations (#3236)

This commit is contained in:
Shlomo
2026-08-06 16:38:46 +03:00
committed by GitHub
parent 3f4e9a9ed9
commit 4ea9ae2036
12 changed files with 183 additions and 62 deletions
+25 -41
View File
@@ -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 כרגע האפליקציה עדיין חייבת להישאר פתוחה והמסך דולק)"
]
}
}
+3
View File
@@ -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"
},
+24 -1
View File
@@ -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": {
+1 -1
View File
@@ -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
+6
View File
@@ -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,
+45 -1
View File
@@ -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);
+3
View File
@@ -16,6 +16,9 @@ Future<void> 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}) {
+21 -2
View File
@@ -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);
}
@@ -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,
);
});
}
}
@@ -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');
});
@@ -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);
}
}
@@ -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,
});
}