mirror of
https://codeberg.org/OpenTracksApp/OpenTracks.git
synced 2026-10-02 17:43:06 +02:00
Merge pull request #1607 from OpenTracksApp/proofOfConcept#1584
Plural quantities via ICU.
This commit is contained in:
+1
-1
@@ -69,7 +69,7 @@ android {
|
||||
|
||||
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
|
||||
|
||||
minSdk 21
|
||||
minSdk 24
|
||||
targetSdk 34
|
||||
|
||||
testInstrumentationRunner "de.dennisguse.opentracks.TestRunner"
|
||||
|
||||
+45
@@ -3,6 +3,7 @@ package de.dennisguse.opentracks.services.announcement;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import android.content.Context;
|
||||
import android.icu.text.MessageFormat;
|
||||
import android.util.Pair;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
@@ -15,6 +16,7 @@ import org.junit.runner.RunWith;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import de.dennisguse.opentracks.LocaleRule;
|
||||
import de.dennisguse.opentracks.content.data.TestDataUtil;
|
||||
@@ -191,6 +193,34 @@ public class VoiceAnnouncementUtilsTest {
|
||||
assertEquals("Total distance 12.4 miles. 1 hour 5 minutes 10 seconds. Average moving speed 11.4 miles per hour.", announcement);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getAnnouncement_imperial_speed_1() {
|
||||
TrackStatistics stats = new TrackStatistics();
|
||||
stats.setTotalDistance(Distance.ofMile(1.1));
|
||||
stats.setTotalTime(Duration.ofHours(2).plusMinutes(5).plusSeconds(10));
|
||||
stats.setMovingTime(Duration.ofHours(1));
|
||||
|
||||
// when
|
||||
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.IMPERIAL_FEET, true, null, null).toString();
|
||||
|
||||
// then
|
||||
assertEquals("Total distance 1.1 miles. 1 hour. Average moving speed 1.1 miles per hour.", announcement);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getAnnouncement_metric_speed_1() {
|
||||
TrackStatistics stats = new TrackStatistics();
|
||||
stats.setTotalDistance(Distance.ofKilometer(1.1));
|
||||
stats.setTotalTime(Duration.ofHours(2).plusMinutes(5).plusSeconds(10));
|
||||
stats.setMovingTime(Duration.ofHours(1));
|
||||
|
||||
// when
|
||||
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.METRIC, true, null, null).toString();
|
||||
|
||||
// then
|
||||
assertEquals("Total distance 1.1 kilometers. 1 hour. Average moving speed 1.1 kilometers per hour.", announcement);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getAnnouncement_withInterval_imperial_speed() {
|
||||
// given
|
||||
@@ -306,4 +336,19 @@ public class VoiceAnnouncementUtilsTest {
|
||||
// then
|
||||
assertEquals(" Current heart rate 133 bpm.", announcement);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ICUMessageDemo() {
|
||||
// Android 7's ICU MessageFormat; working
|
||||
String template = """
|
||||
{n, plural,
|
||||
one {1 mile}
|
||||
other {{n,number,#.#} miles}
|
||||
}""";
|
||||
|
||||
assertEquals("1.1 miles", MessageFormat.format(template, Map.of("n", 1.1)));
|
||||
assertEquals("1 mile", MessageFormat.format(template, Map.of("n", 1)));
|
||||
assertEquals("1.1 miles", MessageFormat.format(template, Map.of("n", 1.11)));
|
||||
assertEquals("1.2 miles", MessageFormat.format(template, Map.of("n", 1.18)));
|
||||
}
|
||||
}
|
||||
|
||||
+20
-16
@@ -9,6 +9,7 @@ import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnno
|
||||
import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceTotalDistance;
|
||||
|
||||
import android.content.Context;
|
||||
import android.icu.text.MessageFormat;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.style.TtsSpan;
|
||||
@@ -17,6 +18,7 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
@@ -45,22 +47,22 @@ class VoiceAnnouncementUtils {
|
||||
switch (unitSystem) {
|
||||
case METRIC -> {
|
||||
perUnitStringId = R.string.voice_per_kilometer;
|
||||
distanceId = R.plurals.voiceDistanceKilometers;
|
||||
speedId = R.plurals.voiceSpeedKilometersPerHour;
|
||||
distanceId = R.string.voiceDistanceKilometersPlural;
|
||||
speedId = R.string.voiceSpeedKilometersPerHourPlural;
|
||||
unitDistanceTTS = "kilometer";
|
||||
unitSpeedTTS = "kilometer per hour";
|
||||
}
|
||||
case IMPERIAL_FEET -> {
|
||||
perUnitStringId = R.string.voice_per_mile;
|
||||
distanceId = R.plurals.voiceDistanceMiles;
|
||||
speedId = R.plurals.voiceSpeedMilesPerHour;
|
||||
distanceId = R.string.voiceDistanceMilesPlural;
|
||||
speedId = R.string.voiceSpeedMilesPerHourPlural;
|
||||
unitDistanceTTS = "mile";
|
||||
unitSpeedTTS = "mile per hour";
|
||||
}
|
||||
case NAUTICAL_IMPERIAL -> {
|
||||
perUnitStringId = R.string.voice_per_nautical_mile;
|
||||
distanceId = R.plurals.voiceDistanceNauticalMiles;
|
||||
speedId = R.plurals.voiceSpeedMKnots;
|
||||
distanceId = R.string.voiceDistanceNauticalMilesPlural;
|
||||
speedId = R.string.voiceSpeedMKnotsPlural;
|
||||
unitDistanceTTS = "nautical mile";
|
||||
unitSpeedTTS = "knots";
|
||||
}
|
||||
@@ -73,7 +75,8 @@ class VoiceAnnouncementUtils {
|
||||
builder.append(context.getString(R.string.total_distance));
|
||||
// Units should always be english singular for TTS.
|
||||
// See https://developer.android.com/reference/android/text/style/TtsSpan?hl=en#TYPE_MEASURE
|
||||
appendDecimalUnit(builder, context.getResources().getQuantityString(distanceId, getQuantityCount(distanceInUnit), distanceInUnit), distanceInUnit, 1, unitDistanceTTS);
|
||||
String template = context.getResources().getString(distanceId);
|
||||
appendDecimalUnit(builder, MessageFormat.format(template, Map.of("n", distanceInUnit)), distanceInUnit, 1, unitDistanceTTS);
|
||||
// Punctuation helps introduce natural pauses in TTS
|
||||
builder.append(".");
|
||||
}
|
||||
@@ -93,7 +96,8 @@ class VoiceAnnouncementUtils {
|
||||
double speedInUnit = averageMovingSpeed.to(unitSystem);
|
||||
builder.append(" ")
|
||||
.append(context.getString(R.string.speed));
|
||||
appendDecimalUnit(builder, context.getResources().getQuantityString(speedId, getQuantityCount(speedInUnit), speedInUnit), speedInUnit, 1, unitSpeedTTS);
|
||||
String template = context.getResources().getString(speedId);
|
||||
appendDecimalUnit(builder, MessageFormat.format(template, Map.of("n", speedInUnit)), speedInUnit, 1, unitSpeedTTS);
|
||||
builder.append(".");
|
||||
}
|
||||
if (shouldVoiceAnnounceLapSpeedPace() && currentDistancePerTime != null) {
|
||||
@@ -101,7 +105,8 @@ class VoiceAnnouncementUtils {
|
||||
if (currentDistancePerTimeInUnit > 0) {
|
||||
builder.append(" ")
|
||||
.append(context.getString(R.string.lap_speed));
|
||||
appendDecimalUnit(builder, context.getResources().getQuantityString(speedId, getQuantityCount(currentDistancePerTimeInUnit), currentDistancePerTimeInUnit), currentDistancePerTimeInUnit, 1, unitSpeedTTS);
|
||||
String template = context.getResources().getString(speedId);
|
||||
appendDecimalUnit(builder, MessageFormat.format(template, Map.of("n", currentDistancePerTimeInUnit)), currentDistancePerTimeInUnit, 1, unitSpeedTTS);
|
||||
builder.append(".");
|
||||
}
|
||||
}
|
||||
@@ -147,23 +152,22 @@ class VoiceAnnouncementUtils {
|
||||
return builder;
|
||||
}
|
||||
|
||||
static int getQuantityCount(double d) {
|
||||
return (int) d;
|
||||
}
|
||||
|
||||
private static void appendDuration(@NonNull Context context, @NonNull SpannableStringBuilder builder, @NonNull Duration duration) {
|
||||
int hours = (int) (duration.toHours());
|
||||
int minutes = (int) (duration.toMinutes() % 60);
|
||||
int seconds = (int) (duration.getSeconds() % 60);
|
||||
|
||||
if (hours > 0) {
|
||||
appendDecimalUnit(builder, context.getResources().getQuantityString(R.plurals.voiceHours, hours, hours), hours, 0, "hour");
|
||||
String template = context.getResources().getString(R.string.voiceHoursPlural);
|
||||
appendDecimalUnit(builder, MessageFormat.format(template, Map.of("n", hours)), hours, 0, "hour");
|
||||
}
|
||||
if (minutes > 0) {
|
||||
appendDecimalUnit(builder, context.getResources().getQuantityString(R.plurals.voiceMinutes, minutes, minutes), minutes, 0, "minute");
|
||||
String template = context.getResources().getString(R.string.voiceMinutesPlural);
|
||||
appendDecimalUnit(builder, MessageFormat.format(template, Map.of("n", minutes)), minutes, 0, "minute");
|
||||
}
|
||||
if (seconds > 0 || duration.isZero()) {
|
||||
appendDecimalUnit(builder, context.getResources().getQuantityString(R.plurals.voiceSeconds, seconds, seconds), seconds, 0, "second");
|
||||
String template = context.getResources().getString(R.string.voiceSecondsPlural);
|
||||
appendDecimalUnit(builder, MessageFormat.format(template, Map.of("n", seconds)), seconds, 0, "second");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -554,29 +554,65 @@ limitations under the License.
|
||||
<string name="value_smallest_recommended">Smallest (recommended)</string>
|
||||
<!-- Voice -->
|
||||
<plurals name="voiceHours">
|
||||
<item quantity="one">1 hour</item>
|
||||
<item quantity="other">%1$d hours</item>
|
||||
<item quantity="one">1 hour [Replaced by VoiceHoursPlural]</item>
|
||||
<item quantity="other">%1$d hours [Replaced by VoiceHoursPlural]</item>
|
||||
</plurals>
|
||||
<plurals name="voiceMinutes">
|
||||
<item quantity="one">1 minute</item>
|
||||
<item quantity="other">%1$d minutes</item>
|
||||
<item quantity="one">1 minute [Replaced by VoiceMinutesPlural]</item>
|
||||
<item quantity="other">%1$d minutes [Replaced by VoiceMinutesPlural]</item>
|
||||
</plurals>
|
||||
<plurals name="voiceSeconds">
|
||||
<item quantity="one">1 second</item>
|
||||
<item quantity="other">%1$d seconds</item>
|
||||
<item quantity="one">1 second [Replaced by voiceSecondsPlural]</item>
|
||||
<item quantity="other">%1$d seconds [Replaced by voiceSecondsPlural]</item>
|
||||
</plurals>
|
||||
<plurals name="voiceSpeedKilometersPerHour">
|
||||
<item quantity="one">1 kilometer per hour</item>
|
||||
<item quantity="other">%1$.1f kilometers per hour</item>
|
||||
<item quantity="one">1 kilometer per hour [Replaced by voiceSpeedKilometersPerHourPlural]</item>
|
||||
<item quantity="other">%1$.1f kilometers per hour [Replaced by voiceSpeedKilometersPerHourPlural]</item>
|
||||
</plurals>
|
||||
<plurals name="voiceSpeedMilesPerHour">
|
||||
<item quantity="one">1 mile per hour</item>
|
||||
<item quantity="other">%1$.1f miles per hour</item>
|
||||
<item quantity="one">1 mile per hour [Replaced by voiceSpeedMilesPerHourPlural]</item>
|
||||
<item quantity="other">%1$.1f miles per hour [Replaced by voiceSpeedMilesPerHourPlural]</item>
|
||||
</plurals>
|
||||
<plurals name="voiceSpeedMKnots">
|
||||
<item quantity="one">1 knot</item>
|
||||
<item quantity="other">%1$.1f knots</item>
|
||||
<item quantity="one">1 knot [Replaced by voiceSpeedMKnotsPlural]</item>
|
||||
<item quantity="other">%1$.1f knots [Replaced by voiceSpeedMKnotsPlural]</item>
|
||||
</plurals>
|
||||
<string name="voiceHoursPlural">
|
||||
{n, plural,
|
||||
=1 {1 hour}
|
||||
other {{n,number} hours}
|
||||
}
|
||||
</string>
|
||||
<string name="voiceMinutesPlural">
|
||||
{n, plural,
|
||||
=1 {1 minute}
|
||||
other {{n,number} minutes}
|
||||
}
|
||||
</string>
|
||||
<string name="voiceSecondsPlural">
|
||||
{n, plural,
|
||||
=1 {1 second}
|
||||
other {{n,number} seconds}
|
||||
}
|
||||
</string>
|
||||
<string name="voiceSpeedKilometersPerHourPlural">
|
||||
{n, plural,
|
||||
=1 {1 kilometer per hour}
|
||||
other {{n,number,#.0} kilometers per hour}
|
||||
}
|
||||
</string>
|
||||
<string name="voiceSpeedMilesPerHourPlural">
|
||||
{n, plural,
|
||||
=1 {1 mile per hour}
|
||||
other {{n,number,#.0} miles per hour}
|
||||
}
|
||||
</string>
|
||||
<string name="voiceSpeedMKnotsPlural">
|
||||
{n, plural,
|
||||
=1 {1 knot}
|
||||
other {{n,number,#.0} knots}
|
||||
}
|
||||
</string>
|
||||
<string name="voice_on_device_speaker_title">Use the device\'s speaker</string>
|
||||
<string name="voice_per_kilometer">per kilometer</string>
|
||||
<string name="voice_per_mile">per mile</string>
|
||||
@@ -587,17 +623,35 @@ limitations under the License.
|
||||
<string name="speed">Average moving speed</string>
|
||||
<string name="total_distance">Total distance</string>
|
||||
<plurals name="voiceDistanceKilometers">
|
||||
<item quantity="one">1 kilometer</item>
|
||||
<item quantity="other">%1$.1f kilometers</item>
|
||||
<item quantity="one">1 kilometer [Replaced by voiceDistanceKilometersPlural]</item>
|
||||
<item quantity="other">%1$.1f kilometers [Replaced by voiceDistanceKilometersPlural]</item>
|
||||
</plurals>
|
||||
<plurals name="voiceDistanceMiles">
|
||||
<item quantity="one">1 mile</item>
|
||||
<item quantity="other">%1$.1f miles</item>
|
||||
<item quantity="one">1 mile [Replaced by voiceDistanceMilesPlural]</item>
|
||||
<item quantity="other">%1$.1f miles [Replaced by voiceDistanceMilesPlural]</item>
|
||||
</plurals>
|
||||
<plurals name="voiceDistanceNauticalMiles">
|
||||
<item quantity="one">1 nautical mile</item>
|
||||
<item quantity="other">%1$.1f nautical miles</item>
|
||||
<item quantity="one">1 nautical mile [Replaced by voiceDistanceNauticalMilesPlural]</item>
|
||||
<item quantity="other">%1$.1f nautical miles [Replaced by voiceDistanceNauticalMilesPlural]</item>
|
||||
</plurals>
|
||||
<string name="voiceDistanceKilometersPlural">
|
||||
{n, plural,
|
||||
=1 {1 kilometer}
|
||||
other {{n,number,#.0} kilometers}
|
||||
}
|
||||
</string>
|
||||
<string name="voiceDistanceMilesPlural">
|
||||
{n, plural,
|
||||
=1 {1 mile}
|
||||
other {{n,number,#.0} miles}
|
||||
}
|
||||
</string>
|
||||
<string name="voiceDistanceNauticalMilesPlural">
|
||||
{n, plural,
|
||||
=1 {1 nautical mile}
|
||||
other {{n,number,#.0} nautical miles}
|
||||
}
|
||||
</string>
|
||||
<string name="average_heart_rate">Average heart rate</string>
|
||||
<string name="current_heart_rate">Current heart rate</string>
|
||||
<!-- Waypoint Type -->
|
||||
|
||||
Reference in New Issue
Block a user