Андрей Субботин "Автоматизация локализации...
DESCRIPTION
Андрей Субботин рассказал про ужасы локализации и как с ними бороться на пошаговом примере: от «Эврика, нам нужно перевести проект на язык Х!» до «Как не прострелить себе ногу, когда у вас есть Xcode, разработчики, переводчики и дедлайн». Были рассмотрены все базовые инструментаы локализации (genstrings, ibtool) и способы их использования.TRANSCRIPT
2
Зачем?
Как это? Кто?
Что это?
Яндекс. Страшное
Полезное Забавное
3
Зачем нужна локализация?
4
Can't read,won't buy.
52,4% не покупают продукт на чужом языке.
60% — для Франции, Японии и России.
89,3% — если английский знают плохо.
5
7
Интернационализация i18n
= подготовка продукта к локализации
8
Локализация L10n
= адаптация продукта к конкретному языку и
местности
Понятный язык интерфейса.
9
Дата и время в привычном формате.
10
Корректная сортировка списков.
11
Поддержка местных единиц измерения.
12
Правильное форматирование чисел.
13
Что локализуется в приложении?
14
Текстовые строки.
15
XIB-файлы.
16
Изображения, аудио.
17
Как приложение подгружает ресурсы?
18
19
en.lproj
20
ru.lproj
21
Подготовка строк к локализации
22
= ваш друг!
23
NSLocalizedString
NSLocalizedString(@"key", @"translator comment")
24
NSLog(NSLocalizedString(@"Some sample text", @"A text string to be output to the logs."));
25
2012-03-06 08:10:05.433 L10nSample[15433:f803] Some sample text
2012-03-06 08:11:02.117 L10nSample[15438:f903] Некий примерный текст
26
= тоже ваш друг!
27
NSFormatter
→ Data Formatting Guide
Выделение строк
28
*.m → Localizable.strings
$ genstrings *.m -o Resources/en.lproj
29
en.lproj/Localizable.strings
/* A text string to be output to the logs. */
"Some sample text" = "Some sample text";
/* A text string to be output to the logs. */
"Some sample text" = "Некий примерный текст";
30
ru.lproj/Localizable.strings
Выделение строк из XIB
31
$ ibtool --export-strings-file \en.lproj/ViewController.strings \en.lproj/ViewController.xib
32
*.xib → *.strings
/* Class = "IBUIButton"; normalTitle = "Welcome!"; ObjectID = "8"; */
"8.normalTitle" = "Welcome!";
33
en.lproj/ViewController.strings
Вмерживание переводов обратно
34
$ ibtool --import-strings-file \en.lproj/ViewController.strings \en.lproj/ViewController.xib \--write en.lproj/ViewController.xib
35
*.strings → *.xib
Инкрементальное обновление XIB'ов
36
Создали en.XIB.
37
А теперь что?!
Локализовали en.XIB → ru.XIB.Добавили новую кнопку в en.XIB.
$ ibtool--previous-file en.lproj/Window.old.xib --incremental-file ru.lproj/Window.old.xib --strings-file ru.lproj/Window.strings --localize-incremental--write ru.lproj/Window.xib en.lproj/Window.new.xib
38
en.xib → ru.xib
$ ibtool--previous-file en.lproj/Window.old.xib --incremental-file ru.lproj/Window.old.xib --strings-file ru.lproj/Window.strings --localize-incremental--write ru.lproj/Window.xib en.lproj/Window.new.xib
39
en.xib → ru.xib
$ ibtool--previous-file en.lproj/Window.old.xib --incremental-file ru.lproj/Window.old.xib --strings-file ru.lproj/Window.strings --localize-incremental--write ru.lproj/Window.xib en.lproj/Window.new.xib
40
en.xib → ru.xib
$ ibtool--previous-file en.lproj/Window.old.xib --incremental-file ru.lproj/Window.old.xib --strings-file ru.lproj/Window.strings --localize-incremental--write ru.lproj/Window.xib en.lproj/Window.new.xib
41
en.xib → ru.xib
$ ibtool--previous-file en.lproj/Window.old.xib --incremental-file ru.lproj/Window.old.xib --strings-file ru.lproj/Window.strings --localize-incremental--write ru.lproj/Window.xib en.lproj/Window.new.xib
42
en.xib → ru.xib
$ ibtool--previous-file en.lproj/Window.old.xib --incremental-file ru.lproj/Window.old.xib --strings-file ru.lproj/Window.strings --localize-incremental--write ru.lproj/Window.xib en.lproj/Window.new.xib
43
en.xib → ru.xib
$ ibtool--previous-file en.lproj/Window.old.xib --incremental-file ru.lproj/Window.old.xib --strings-file ru.lproj/Window.strings --localize-incremental--write ru.lproj/Window.xib en.lproj/Window.new.xib
44
en.xib → ru.xib
Храните предыдущиеверсии XIB файлов.
45
Не правьте рукамилокализованные XIB файлы.
46
Переводчики
47
понимают английский.
48
Переводчики
не всегда понимают русский.
49
Переводчики
не используют Xcode и не редактируют XIB.
50
Переводчики
Не знают контекста переводабез вашей помощи.
51
Переводчики
Как ониработают?
52
но редко.
53
Вместе с вами,
Очень часто
54
по e-mail.
+1 к карме
55
translations.launchpad.net
56
Tanker
= web-сервис= API для загрузки и выгрузки переводов
Yoda
57
Babelfish
Babelyoda
58
избавляет отрутинной ручной работы
сводит количество багов при локализации к
минимуму
59
генерирует .stringsиз кода и XIB’ов загружает .strings
в Tanker
забирает из Tanker’асвежие переводы
обновляетXIB файлы
аккуратно всекоммитит в git
PROFIT!!
Babelyoda
60
= библиотека для работы с .strings, genstrings и ibtool
Babelfile
61
...по аналогии с Makefile, Gemfile, Rakefile и т.п.
= единое место конфигурации.
Babelyoda::Specification.new do |s| s.name = 'YandexMaps' s.development_language = :en s.localization_languages = [:ru, :uk, :tr] s.engine = Babelyoda::Tanker.new do |t| t.token = ENV['TANKER_TOKEN'] t.project_id = 'myak_iphone' t.endpoint = ENV['TANKER_HOST'] end s.scm = Babelyoda::Git.new s.source_files = FileList['{Classes,Shared}/**/*.{m,mm,h}'] s.resources_folder = 'Resources' s.xib_files = FileList['Resources/**/en.lproj/*.xib'] s.strings_files = FileList['Resources/**/en.lproj/*.strings']end
62
Babelyoda::Specification.new do |s| s.name = 'YandexMaps' s.development_language = :en s.localization_languages = [:ru, :uk, :tr] s.engine = Babelyoda::Tanker.new do |t| t.token = ENV['TANKER_TOKEN'] t.project_id = 'myak_iphone' t.endpoint = ENV['TANKER_HOST'] end s.scm = Babelyoda::Git.new s.source_files = FileList['{Classes,Shared}/**/*.{m,mm,h}'] s.resources_folder = 'Resources' s.xib_files = FileList['Resources/**/en.lproj/*.xib'] s.strings_files = FileList['Resources/**/en.lproj/*.strings']end
63
Babelyoda::Specification.new do |s| s.name = 'YandexMaps' s.development_language = :en s.localization_languages = [:ru, :uk, :tr] s.engine = Babelyoda::Tanker.new do |t| t.token = ENV['TANKER_TOKEN'] t.project_id = 'myak_iphone' t.endpoint = ENV['TANKER_HOST'] end s.scm = Babelyoda::Git.new s.source_files = FileList['{Classes,Shared}/**/*.{m,mm,h}'] s.resources_folder = 'Resources' s.xib_files = FileList['Resources/**/en.lproj/*.xib'] s.strings_files = FileList['Resources/**/en.lproj/*.strings']end
64
Babelyoda::Specification.new do |s| s.name = 'YandexMaps' s.development_language = :en s.localization_languages = [:ru, :uk, :tr] s.engine = Babelyoda::Tanker.new do |t| t.token = ENV['TANKER_TOKEN'] t.project_id = 'myak_iphone' t.endpoint = ENV['TANKER_HOST'] end s.scm = Babelyoda::Git.new s.source_files = FileList['{Classes,Shared}/**/*.{m,mm,h}'] s.resources_folder = 'Resources' s.xib_files = FileList['Resources/**/en.lproj/*.xib'] s.strings_files = FileList['Resources/**/en.lproj/*.strings']end
65
Babelyoda::Specification.new do |s| s.name = 'YandexMaps' s.development_language = :en s.localization_languages = [:ru, :uk, :tr] s.engine = Babelyoda::Tanker.new do |t| t.token = ENV['TANKER_TOKEN'] t.project_id = 'myak_iphone' t.endpoint = ENV['TANKER_HOST'] end s.scm = Babelyoda::Git.new s.source_files = FileList['{Classes,Shared}/**/*.{m,mm,h}'] s.resources_folder = 'Resources' s.xib_files = FileList['Resources/**/en.lproj/*.xib'] s.strings_files = FileList['Resources/**/en.lproj/*.strings']end
66
One command to rule them all!
67
$ rake babelyoda
$ rake -Trake babelyodarake babelyoda:create_keysetsrake babelyoda:drop_empty_stringsrake babelyoda:drop_orphan_keysrake babelyoda:drop_orphan_keysetsrake babelyoda:extractrake babelyoda:extract_stringsrake babelyoda:extract_xib_stringsrake babelyoda:fetch_stringsrake babelyoda:initBabelfilerake babelyoda:localize_xibsrake babelyoda:pullrake babelyoda:pushrake babelyoda:remote:drop_keysetsrake babelyoda:remote:listrake babelyoda:verify
68
#!/bin/bash
function verify { if [ $CONFIGURATION == 'AppStore' ] ; then rvm rvmrc trust . && rvm rvmrc load . && bundle \ && bundle exec rake babelyoda:verify return $? fi return 0}git submodule update --init --recursive && verify
69
yxbuildkit-prebuild.sh
https://github.com/eploko/babelyoda
70
Available on GitHub!
Плюрализация
71
хоррор стори
I scanned 12 directories.
72
NSLog(@"I scanned %g directories.", directoryCount);
73
I scanned 1 directories.
74
NSLog(@"I scanned %g %@.", directoryCount, directoryCount == 1 ? @"directory" : @"directories", );
75
I scanned 1 directory.
76
NSLog( NSLocalizedString(@"I scanned %g %@.", @”Text to show the number of directories scanned”), dirScanCount, dirScanCount == 1 ? NSLocalizedString(@"directory", @”Single directory”) : NSLocalizedString(@"directories", @”Plural directories”) );
77
78
Как это видит переводчик?
79
"I scanned %g %@."
"directory"
"directories"
80
"Я просканировал %g %@."
"папка"
"каталоги"
Я отсканировал 1 папка.
81
Я отсканировал 5 каталоги.
82
NSLog( dirScanCount == 1 ? NSLocalizedString("I scanned %g directory.", @”Blah”) : NSLocalizedString("I scanned %g directories.", @”Blah”), dirScanCount );
83
“It is more complicated than you think.”— The Eighth Networking Truth, from RFC 1925
84
NSString *pluralTransfers = NSLocalizedString(@"%d changes", @"The number of changes shown in the route description");
85
NSString *forms[4] = {0};forms[0] = NSLocalizedString(@"%d change", @"Blah");forms[1] = NSLocalizedString(@"%d changes", @"Blah");forms[2] = NSLocalizedString(@"%d changes", @"Blah");forms[3] = NSLocalizedString(@"%d changes", @"Blah"); int form = YXPluralFormForN(self.transfersCount);NSString *pluralTransfers = forms[i];
86
int YXPluralFormForRU(NSInteger n){ // One - 1, 21, 31, ... // Some - 2-4, 22-24, 32-34 ... // Many - 5-20, 25-30, ... NSInteger n10 = n % 10; if ((n10 == 1) && ((n == 1) || (n > 20))) { return 0; } else if ((n10 > 1) && (n10 < 5) && ((n > 20) || (n < 10))) { return 1; } else { return 2; } }
87
88
Как это видит переводчик?
89
"%d change"
"%d changes"
"%d changes"
"%d changes"
NSString *forms[4] = {0};forms[0] = NSLocalizedString(@"NumberChanges0", @"Blah");forms[1] = NSLocalizedString(@"NumberChanges1", @"Blah");forms[2] = NSLocalizedString(@"NumberChanges2", @"Blah");forms[3] = NSLocalizedString(@"NumberChanges3", @"Blah"); int form = YXPluralFormForN(self.transfersCount);NSString *pluralTransfers = forms[i];
90
91
Как это видит переводчик?
92
"NumberChanges0"
"NumberChanges3"
"NumberChanges2"
"NumberChanges1"
genstrings “магия”
93
NSLocalizedString(@"%[one, some, many, none]d changes", @"The number of changes shown in the route description");
94
/* The number of changes shown in the route description */"%[one]d changes" = "%d changes";"%[some]d changes" = "%d changes";"%[many]d changes" = "%d changes";"%[none]d changes" = "%d changes";
95
Localizable.strings
/* The number of changes shown in the route description */"%[one]d changes" = "%d остановка";"%[some]d changes" = "%d остановки";"%[many]d changes" = "%d остановок";"%[none]d changes" = "";
96
Localizable.strings
NSString *YXPluralFormForRU(NSInteger n){ // One - 1, 21, 31, ... // Some - 2-4, 22-24, 32-34 ... // Many - 5-20, 25-30, ... NSInteger n10 = n % 10; if ((n10 == 1) && ((n == 1) || (n > 20))) { return @”[one]”; } else if ((n10 > 1) && (n10 < 5) && ((n > 20) || (n < 10))) { return @”[some]”; } else { return @”[many]”; } }
97
NSString *pluralKey = NSLocalizedString( @"%[one, some, many, none]d changes", @"The number of changes shown in the route description");
NSString *pluralTransfers = YXLocalizedStringN(pluralKey, self.transfersCount);
98
"%[one, some, many, none]d changes"
"%[some]d changes"
“Хитрости”
100
101
Английский текст в качестве ключа
NSLocalizedString(@"Tap Here", @"Action button title");
NSLocalizedString(@"TapButtonTitle", @"Action button title");
102
Английский текст в качестве ключа
WelcomeButtonTitleWelcomeTitleButtonTitleWelcomeWelcomeTITLEWelcomeBtnTitle
103
Различные контексты
Edit = ПравитьEdit = ИзменитьEdit = Переименовать
104
Различные контексты
NSLocalizedStringFromTable(<#key#>, <#tbl#>, <#comment#>)
NSLocalizedStringFromTable(@”Edit”,@”Common”,@”Blah”)
NSLocalizedStringFromTable(@”Edit”,@”Buttons”,@”Blah”)
105
Склеивание строк
NSString *part1 = NSLocalizedString(@"People in the room", @"Part 1");NSString *part2 = NSLocalizedString(@"%d", @"Part 2");NSString *halfResult = [NSString stringWithFormat:@"%@: %@", part1, part2];NSString *result = [NSString stringWithFormat:halfResult, 5];
へやへ:5人
106
Склеивание строк
NSLocalizedString( @"People in the room: %[one, some, many, none]d", @"Blah blah");
へやへ:5人
107
Полезные проекты
108
Linguan
109
Wincent Strings Utility
“Merges, extracts and combines .string files (for incremental localization)”— http://wincent.com/a/products/wincent-strings-util/
110
genstrings2
“40x faster than genstrings”— http://www.cocoanetics.com/2012/01/genstrings2/
111
Twine
“String Management for iOS, Mac OS X, and Android Development”— http://www.mobiata.com/blog/2012/02/08/twine-string-management-ios-mac-os-x
112
WTF!?
113
WTF!?
114
WTF!?
115
WTF!?
116
WTF!?
117
WTF!?
WTF!?
WTF!?
118
WTF!?
119
WTF!?
WTF!?
WTF!?
WTF!?
WTF!?
120
WTF!? WTF!?