Repo created
This commit is contained in:
parent
4af19165ec
commit
68073add76
12458 changed files with 12350765 additions and 2 deletions
19
iphone/Maps/Core/TextToSpeech/MWMTextToSpeech+CPP.h
Normal file
19
iphone/Maps/Core/TextToSpeech/MWMTextToSpeech+CPP.h
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
#import "MWMTextToSpeech.h"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@interface MWMTextToSpeech (CPP)
|
||||
|
||||
// Returns a list of available languages in the following format:
|
||||
// * name in bcp47;
|
||||
// * localized name;
|
||||
- (std::vector<std::pair<std::string, std::string>>)availableLanguages;
|
||||
- (std::pair<std::string, std::string>)standardLanguage;
|
||||
|
||||
@end
|
||||
|
||||
namespace tts
|
||||
{
|
||||
std::string translateLocale(std::string const & localeString);
|
||||
} // namespace tts
|
||||
37
iphone/Maps/Core/TextToSpeech/MWMTextToSpeech.h
Normal file
37
iphone/Maps/Core/TextToSpeech/MWMTextToSpeech.h
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
#import "MWMTextToSpeechObserver.h"
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
|
||||
@interface MWMTextToSpeech : NSObject
|
||||
|
||||
+ (MWMTextToSpeech *)tts;
|
||||
- (AVSpeechSynthesisVoice *)voice;
|
||||
+ (BOOL)isTTSEnabled;
|
||||
+ (void)setTTSEnabled:(BOOL)enabled;
|
||||
+ (BOOL)isStreetNamesTTSEnabled;
|
||||
+ (void)setStreetNamesTTSEnabled:(BOOL)enabled;
|
||||
+ (NSDictionary<NSString *, NSString *> *)availableLanguages;
|
||||
+ (NSString *)selectedLanguage;
|
||||
+ (NSString *)savedLanguage;
|
||||
+ (NSInteger)speedCameraMode;
|
||||
+ (void)setSpeedCameraMode:(NSInteger)speedCameraMode;
|
||||
+ (void)playTest;
|
||||
|
||||
+ (void)addObserver:(id<MWMTextToSpeechObserver>)observer;
|
||||
+ (void)removeObserver:(id<MWMTextToSpeechObserver>)observer;
|
||||
|
||||
+ (void)applicationDidBecomeActive;
|
||||
|
||||
@property(nonatomic) BOOL active;
|
||||
- (void)setNotificationsLocale:(NSString *)locale;
|
||||
- (void)playTurnNotifications:(NSArray<NSString *> *)turnNotifications;
|
||||
- (void)playWarningSound;
|
||||
- (void)play:(NSString *)text;
|
||||
|
||||
- (instancetype)init __attribute__((unavailable("call +tts instead")));
|
||||
- (instancetype)copy __attribute__((unavailable("call +tts instead")));
|
||||
- (instancetype)copyWithZone:(NSZone *)zone __attribute__((unavailable("call +tts instead")));
|
||||
+ (instancetype)allocWithZone:(struct _NSZone *)zone
|
||||
__attribute__((unavailable("call +tts instead")));
|
||||
+ (instancetype) new __attribute__((unavailable("call +tts instead")));
|
||||
|
||||
@end
|
||||
392
iphone/Maps/Core/TextToSpeech/MWMTextToSpeech.mm
Normal file
392
iphone/Maps/Core/TextToSpeech/MWMTextToSpeech.mm
Normal file
|
|
@ -0,0 +1,392 @@
|
|||
#import <AVFoundation/AVFoundation.h>
|
||||
#import "MWMRouter.h"
|
||||
#import "MWMTextToSpeech+CPP.h"
|
||||
#import "SwiftBridge.h"
|
||||
#import "TTSTester.h"
|
||||
|
||||
#include "LocaleTranslator.h"
|
||||
|
||||
#include <CoreApi/Framework.h>
|
||||
|
||||
#include "platform/languages.hpp"
|
||||
|
||||
using namespace locale_translator;
|
||||
using namespace routing;
|
||||
|
||||
namespace
|
||||
{
|
||||
NSString * const kUserDefaultsTTSLanguageBcp47 = @"UserDefaultsTTSLanguageBcp47";
|
||||
NSString * const kIsTTSEnabled = @"UserDefaultsNeedToEnableTTS";
|
||||
NSString * const kIsStreetNamesTTSEnabled = @"UserDefaultsNeedToEnableStreetNamesTTS";
|
||||
NSString * const kDefaultLanguage = @"en-US";
|
||||
|
||||
std::vector<std::pair<std::string, std::string>> availableLanguages()
|
||||
{
|
||||
NSArray<AVSpeechSynthesisVoice *> * voices = [AVSpeechSynthesisVoice speechVoices];
|
||||
std::vector<std::pair<std::string, std::string>> native;
|
||||
for (AVSpeechSynthesisVoice * v in voices)
|
||||
native.emplace_back(make_pair(bcp47ToTwineLanguage(v.language), v.language.UTF8String));
|
||||
|
||||
using namespace routing::turns::sound;
|
||||
std::vector<std::pair<std::string, std::string>> result;
|
||||
for (auto const & [twineRouting, _] : kLanguageList)
|
||||
{
|
||||
for (auto const & [twineVoice, bcp47Voice] : native)
|
||||
{
|
||||
if (twineVoice == twineRouting)
|
||||
{
|
||||
auto pair = std::make_pair(bcp47Voice, tts::translateLocale(bcp47Voice));
|
||||
if (std::find(result.begin(), result.end(), pair) == result.end())
|
||||
result.emplace_back(std::move(pair));
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
using Observer = id<MWMTextToSpeechObserver>;
|
||||
using Observers = NSHashTable<Observer>;
|
||||
} // namespace
|
||||
|
||||
@interface MWMTextToSpeech ()<AVSpeechSynthesizerDelegate>
|
||||
{
|
||||
std::vector<std::pair<std::string, std::string>> _availableLanguages;
|
||||
}
|
||||
|
||||
@property(nonatomic) AVSpeechSynthesizer * speechSynthesizer;
|
||||
@property(nonatomic) AVSpeechSynthesisVoice * speechVoice;
|
||||
@property(nonatomic) AVAudioPlayer * audioPlayer;
|
||||
|
||||
@property(nonatomic) Observers * observers;
|
||||
|
||||
@end
|
||||
|
||||
@implementation MWMTextToSpeech
|
||||
|
||||
+ (MWMTextToSpeech *)tts {
|
||||
static dispatch_once_t onceToken;
|
||||
static MWMTextToSpeech * tts = nil;
|
||||
dispatch_once(&onceToken, ^{
|
||||
tts = [[self alloc] initTTS];
|
||||
});
|
||||
return tts;
|
||||
}
|
||||
|
||||
+ (void)applicationDidBecomeActive {
|
||||
auto tts = [self tts];
|
||||
tts.speechSynthesizer = nil;
|
||||
tts.speechVoice = nil;
|
||||
}
|
||||
|
||||
- (instancetype)initTTS {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_availableLanguages = availableLanguages();
|
||||
_observers = [Observers weakObjectsHashTable];
|
||||
|
||||
NSString * saved = [[self class] savedLanguage];
|
||||
NSString * preferedLanguageBcp47;
|
||||
if (saved.length)
|
||||
preferedLanguageBcp47 = saved;
|
||||
else
|
||||
preferedLanguageBcp47 = [AVSpeechSynthesisVoice currentLanguageCode];
|
||||
|
||||
std::pair<std::string, std::string> const lan =
|
||||
std::make_pair(preferedLanguageBcp47.UTF8String,
|
||||
tts::translateLocale(preferedLanguageBcp47.UTF8String));
|
||||
|
||||
if (find(_availableLanguages.begin(), _availableLanguages.end(), lan) !=
|
||||
_availableLanguages.end())
|
||||
[self setNotificationsLocale:preferedLanguageBcp47];
|
||||
else
|
||||
[self setNotificationsLocale:kDefaultLanguage];
|
||||
|
||||
NSError * err = nil;
|
||||
if (![[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback
|
||||
mode:AVAudioSessionModeVoicePrompt
|
||||
options:AVAudioSessionCategoryOptionInterruptSpokenAudioAndMixWithOthers |
|
||||
AVAudioSessionCategoryOptionDuckOthers
|
||||
error:&err]) {
|
||||
LOG(LWARNING, ("Couldn't configure audio session: ", [err localizedDescription]));
|
||||
}
|
||||
|
||||
// Set initial StreetNamesTTS setting
|
||||
NSDictionary *dictionary = @{ kIsStreetNamesTTSEnabled : @NO };
|
||||
[NSUserDefaults.standardUserDefaults registerDefaults:dictionary];
|
||||
|
||||
self.active = YES;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[[AVAudioSession sharedInstance] setActive:NO
|
||||
withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation
|
||||
error:nil];
|
||||
self.speechSynthesizer.delegate = nil;
|
||||
}
|
||||
- (std::vector<std::pair<std::string, std::string>>)availableLanguages { return _availableLanguages; }
|
||||
- (std::pair<std::string, std::string>)standardLanguage {
|
||||
return std::make_pair(kDefaultLanguage.UTF8String, tts::translateLocale(kDefaultLanguage.UTF8String));
|
||||
}
|
||||
- (void)setNotificationsLocale:(NSString *)locale {
|
||||
NSUserDefaults * ud = NSUserDefaults.standardUserDefaults;
|
||||
[ud setObject:locale forKey:kUserDefaultsTTSLanguageBcp47];
|
||||
[self createVoice:locale];
|
||||
}
|
||||
- (AVSpeechSynthesisVoice *)voice {
|
||||
[self createVoice:[[self class] savedLanguage]];
|
||||
return self.speechVoice;
|
||||
}
|
||||
- (BOOL)isValid { return _speechSynthesizer != nil && _speechVoice != nil; }
|
||||
+ (BOOL)isTTSEnabled { return [NSUserDefaults.standardUserDefaults boolForKey:kIsTTSEnabled]; }
|
||||
+ (void)setTTSEnabled:(BOOL)enabled {
|
||||
if ([self isTTSEnabled] == enabled)
|
||||
return;
|
||||
auto tts = [self tts];
|
||||
if (!enabled)
|
||||
[tts setActive:NO];
|
||||
NSUserDefaults * ud = NSUserDefaults.standardUserDefaults;
|
||||
[ud setBool:enabled forKey:kIsTTSEnabled];
|
||||
|
||||
[tts onTTSStatusUpdated];
|
||||
if (enabled)
|
||||
[tts setActive:YES];
|
||||
}
|
||||
+ (BOOL)isStreetNamesTTSEnabled { return [NSUserDefaults.standardUserDefaults boolForKey:kIsStreetNamesTTSEnabled]; }
|
||||
+ (void)setStreetNamesTTSEnabled:(BOOL)enabled {
|
||||
if ([self isStreetNamesTTSEnabled] == enabled)
|
||||
return;
|
||||
NSUserDefaults * ud = NSUserDefaults.standardUserDefaults;
|
||||
[ud setBool:enabled forKey:kIsStreetNamesTTSEnabled];
|
||||
[ud synchronize];
|
||||
}
|
||||
|
||||
- (void)setActive:(BOOL)active {
|
||||
if (![[self class] isTTSEnabled] || self.active == active)
|
||||
return;
|
||||
if (active && ![self isValid])
|
||||
[self createVoice:[[self class] savedLanguage]];
|
||||
[MWMRouter enableTurnNotifications:active];
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self onTTSStatusUpdated];
|
||||
});
|
||||
}
|
||||
|
||||
- (BOOL)active { return [[self class] isTTSEnabled] && [MWMRouter areTurnNotificationsEnabled]; }
|
||||
|
||||
+ (NSDictionary<NSString *, NSString *> *)availableLanguages
|
||||
{
|
||||
NSMutableDictionary<NSString *, NSString *> *availableLanguages = [[NSMutableDictionary alloc] init];
|
||||
auto const & v = [[self tts] availableLanguages];
|
||||
for (auto i: v) {
|
||||
[availableLanguages setObject:@(i.second.c_str()) forKey:@(i.first.c_str())];
|
||||
}
|
||||
return availableLanguages;
|
||||
}
|
||||
|
||||
+ (NSString *)selectedLanguage {
|
||||
if ([self savedLanguage] != nil) {
|
||||
return [self savedLanguage];
|
||||
}
|
||||
|
||||
NSString * preferedLanguageBcp47 = [AVSpeechSynthesisVoice currentLanguageCode];
|
||||
|
||||
std::pair<std::string, std::string> const lan =
|
||||
std::make_pair(preferedLanguageBcp47.UTF8String, tts::translateLocale(preferedLanguageBcp47.UTF8String));
|
||||
|
||||
std::vector<std::pair<std::string, std::string>> const availableLanguages = [[self tts] availableLanguages];
|
||||
if (find(availableLanguages.begin(), availableLanguages.end(), lan) !=
|
||||
availableLanguages.end()) {
|
||||
return preferedLanguageBcp47;
|
||||
}
|
||||
|
||||
return kDefaultLanguage;
|
||||
}
|
||||
|
||||
+ (NSString *)savedLanguage {
|
||||
return [NSUserDefaults.standardUserDefaults stringForKey:kUserDefaultsTTSLanguageBcp47];
|
||||
}
|
||||
|
||||
+ (NSInteger)speedCameraMode
|
||||
{
|
||||
SpeedCameraManagerMode mode = GetFramework().GetRoutingManager().GetSpeedCamManager().GetMode();
|
||||
if (mode == SpeedCameraManagerMode::Auto) {
|
||||
return 2;
|
||||
} else if (mode == SpeedCameraManagerMode::Always) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
+ (void)setSpeedCameraMode:(NSInteger)speedCameraMode
|
||||
{
|
||||
SpeedCameraManagerMode mode = SpeedCameraManagerMode::Never;
|
||||
if (speedCameraMode == 2) {
|
||||
mode = SpeedCameraManagerMode::Auto;
|
||||
} else if (speedCameraMode == 1) {
|
||||
mode = SpeedCameraManagerMode::Always;
|
||||
}
|
||||
GetFramework().GetRoutingManager().GetSpeedCamManager().SetMode(mode);
|
||||
}
|
||||
|
||||
- (void)createVoice:(NSString *)locale {
|
||||
if (!self.speechSynthesizer) {
|
||||
self.speechSynthesizer = [[AVSpeechSynthesizer alloc] init];
|
||||
self.speechSynthesizer.delegate = self;
|
||||
}
|
||||
|
||||
NSMutableArray<NSString *> * candidateLocales = [@[ kDefaultLanguage, @"en-GB" ] mutableCopy];
|
||||
|
||||
if (locale)
|
||||
[candidateLocales insertObject:locale atIndex:0];
|
||||
else
|
||||
LOG(LWARNING, ("locale is nil. Trying default locale."));
|
||||
|
||||
AVSpeechSynthesisVoice * voice = nil;
|
||||
for (NSString * loc in candidateLocales) {
|
||||
if (@available(iOS 16.0, *)) {
|
||||
for (AVSpeechSynthesisVoice * aVoice in [AVSpeechSynthesisVoice speechVoices]) {
|
||||
if (voice == nil && aVoice.language == loc && aVoice.quality == AVSpeechSynthesisVoiceQualityPremium) {
|
||||
voice = aVoice;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (AVSpeechSynthesisVoice * aVoice in [AVSpeechSynthesisVoice speechVoices]) {
|
||||
if (voice == nil && aVoice.language == loc && aVoice.quality == AVSpeechSynthesisVoiceQualityEnhanced) {
|
||||
voice = aVoice;
|
||||
}
|
||||
}
|
||||
if (voice == nil) {
|
||||
voice = [AVSpeechSynthesisVoice voiceWithLanguage:loc];
|
||||
}
|
||||
if (voice) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
self.speechVoice = voice;
|
||||
if (voice) {
|
||||
std::string const twineLang = bcp47ToTwineLanguage(voice.language);
|
||||
if (twineLang.empty())
|
||||
LOG(LERROR, ("Cannot convert UI locale or default locale to twine language. MWMTextToSpeech "
|
||||
"is invalid."));
|
||||
else
|
||||
[MWMRouter setTurnNotificationsLocale:@(twineLang.c_str())];
|
||||
} else {
|
||||
LOG(LWARNING,
|
||||
("The UI language and English are not available for TTS. MWMTextToSpeech is invalid."));
|
||||
}
|
||||
}
|
||||
|
||||
+ (void)playTest
|
||||
{
|
||||
TTSTester * ttsTester = [[TTSTester alloc] init];
|
||||
[ttsTester playRandomTestString];
|
||||
}
|
||||
|
||||
|
||||
- (void)speakOneString:(NSString *)textToSpeak {
|
||||
AVSpeechUtterance * utterance = [AVSpeechUtterance speechUtteranceWithString:textToSpeak];
|
||||
utterance.voice = self.speechVoice;
|
||||
utterance.rate = AVSpeechUtteranceDefaultSpeechRate;
|
||||
[self.speechSynthesizer speakUtterance:utterance];
|
||||
}
|
||||
|
||||
- (void)playTurnNotifications:(NSArray<NSString *> *)turnNotifications {
|
||||
auto stopSession = ^{
|
||||
if (self.speechSynthesizer.isSpeaking)
|
||||
return;
|
||||
[[AVAudioSession sharedInstance]
|
||||
setActive:NO
|
||||
withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation
|
||||
error:nil];
|
||||
};
|
||||
|
||||
if (![MWMRouter isOnRoute] || !self.active) {
|
||||
stopSession();
|
||||
return;
|
||||
}
|
||||
|
||||
if (![self isValid])
|
||||
[self createVoice:[[self class] savedLanguage]];
|
||||
|
||||
if (![self isValid]) {
|
||||
stopSession();
|
||||
return;
|
||||
}
|
||||
|
||||
if (turnNotifications.count == 0) {
|
||||
stopSession();
|
||||
return;
|
||||
} else {
|
||||
NSError * err = nil;
|
||||
if (![[AVAudioSession sharedInstance] setActive:YES error:&err]) {
|
||||
LOG(LWARNING, ("Couldn't activate audio session: ", [err localizedDescription]));
|
||||
return;
|
||||
}
|
||||
|
||||
for (NSString * notification in turnNotifications)
|
||||
[self speakOneString:notification];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)playWarningSound {
|
||||
if (!GetFramework().GetRoutingManager().GetSpeedCamManager().ShouldPlayBeepSignal())
|
||||
return;
|
||||
|
||||
[self.audioPlayer play];
|
||||
}
|
||||
|
||||
- (AVAudioPlayer *)audioPlayer {
|
||||
if (!_audioPlayer) {
|
||||
if (auto url = [[NSBundle mainBundle] URLForResource:@"Alert 5" withExtension:@"m4a"]) {
|
||||
NSError * error = nil;
|
||||
_audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:&error];
|
||||
CHECK(!error, (error.localizedDescription.UTF8String));
|
||||
} else {
|
||||
CHECK(false, ("Speed warning file not found"));
|
||||
}
|
||||
}
|
||||
|
||||
return _audioPlayer;
|
||||
}
|
||||
|
||||
- (void)play:(NSString *)text {
|
||||
if (![self isValid])
|
||||
[self createVoice:[[self class] savedLanguage]];
|
||||
|
||||
[self speakOneString:text];
|
||||
}
|
||||
|
||||
#pragma mark - MWMNavigationDashboardObserver
|
||||
|
||||
- (void)onTTSStatusUpdated {
|
||||
for (Observer observer in self.observers)
|
||||
[observer onTTSStatusUpdated];
|
||||
}
|
||||
|
||||
#pragma mark - Add/Remove Observers
|
||||
|
||||
+ (void)addObserver:(id<MWMTextToSpeechObserver>)observer {
|
||||
[[self tts].observers addObject:observer];
|
||||
}
|
||||
|
||||
+ (void)removeObserver:(id<MWMTextToSpeechObserver>)observer {
|
||||
[[self tts].observers removeObject:observer];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
namespace tts
|
||||
{
|
||||
std::string translateLocale(std::string const & localeString)
|
||||
{
|
||||
NSString * nsLocaleString = [NSString stringWithUTF8String: localeString.c_str()];
|
||||
NSLocale * locale = [[NSLocale alloc] initWithLocaleIdentifier: nsLocaleString];
|
||||
NSString * localizedName = [locale localizedStringForLocaleIdentifier:nsLocaleString];
|
||||
localizedName = [localizedName capitalizedString];
|
||||
return std::string(localizedName.UTF8String);
|
||||
}
|
||||
} // namespace tts
|
||||
5
iphone/Maps/Core/TextToSpeech/MWMTextToSpeechObserver.h
Normal file
5
iphone/Maps/Core/TextToSpeech/MWMTextToSpeechObserver.h
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
@protocol MWMTextToSpeechObserver<NSObject>
|
||||
|
||||
- (void)onTTSStatusUpdated;
|
||||
|
||||
@end
|
||||
10
iphone/Maps/Core/TextToSpeech/TTSTester.h
Normal file
10
iphone/Maps/Core/TextToSpeech/TTSTester.h
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface TTSTester : NSObject
|
||||
|
||||
- (void)playRandomTestString;
|
||||
- (NSArray<NSString *> *)getTestStrings:(NSString *)language;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
59
iphone/Maps/Core/TextToSpeech/TTSTester.mm
Normal file
59
iphone/Maps/Core/TextToSpeech/TTSTester.mm
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
#import "TTSTester.h"
|
||||
|
||||
#include "LocaleTranslator.h"
|
||||
#include "MWMTextToSpeech.h"
|
||||
|
||||
#include "base/logging.hpp"
|
||||
|
||||
@implementation TTSTester
|
||||
|
||||
static NSString * const NotFoundDelimiter = @"__not_found__";
|
||||
|
||||
NSArray<NSString *> * testStrings;
|
||||
NSString * testStringsLanguage;
|
||||
|
||||
int testStringIndex;
|
||||
|
||||
- (void)playRandomTestString {
|
||||
NSString * currentTTSLanguage = MWMTextToSpeech.savedLanguage;
|
||||
if (testStrings == nil || ![currentTTSLanguage isEqualToString:testStringsLanguage]) {
|
||||
testStrings = [self getTestStrings:currentTTSLanguage];
|
||||
if (testStrings == nil) {
|
||||
LOG(LWARNING, ("Couldn't load TTS test strings"));
|
||||
return;
|
||||
}
|
||||
testStringsLanguage = currentTTSLanguage;
|
||||
}
|
||||
|
||||
[[MWMTextToSpeech tts] play:testStrings[testStringIndex]];
|
||||
|
||||
if (++testStringIndex >= testStrings.count)
|
||||
testStringIndex = 0;
|
||||
}
|
||||
|
||||
- (NSArray<NSString *> *)getTestStrings:(NSString *)language {
|
||||
NSString * twineLanguage = [NSString stringWithUTF8String:locale_translator::bcp47ToTwineLanguage(language).c_str()];
|
||||
NSString * languagePath = [NSBundle.mainBundle pathForResource:twineLanguage ofType:@"lproj"];
|
||||
if (languagePath == nil) {
|
||||
LOG(LWARNING, ("Couldn't find translation file for ", twineLanguage.UTF8String));
|
||||
return nil;
|
||||
}
|
||||
NSBundle * bundle = [NSBundle bundleWithPath:languagePath];
|
||||
|
||||
NSMutableArray * appTips = [NSMutableArray new];
|
||||
for (int idx = 0; ; idx++) {
|
||||
NSString * appTipKey = [NSString stringWithFormat:@"app_tip_%02d", idx];
|
||||
NSString * appTip = [bundle localizedStringForKey:appTipKey value:NotFoundDelimiter table:nil];
|
||||
if ([appTip isEqualToString:NotFoundDelimiter])
|
||||
break;
|
||||
[appTips addObject:appTip];
|
||||
}
|
||||
|
||||
// shuffle
|
||||
for (NSUInteger i = appTips.count; i > 1; i--)
|
||||
[appTips exchangeObjectAtIndex:i - 1 withObjectAtIndex:arc4random_uniform((u_int32_t)i)];
|
||||
|
||||
return appTips;
|
||||
}
|
||||
|
||||
@end
|
||||
Loading…
Add table
Add a link
Reference in a new issue