Андрей Субботин "Автоматизация локализации...

Post on 18-Jan-2015

1.297 views 3 download

description

Андрей Субботин рассказал про ужасы локализации и как с ними бороться на пошаговом примере: от «Эврика, нам нужно перевести проект на язык Х!» до «Как не прострелить себе ногу, когда у вас есть Xcode, разработчики, переводчики и дедлайн». Были рассмотрены все базовые инструментаы локализации (genstrings, ibtool) и способы их использования.

Transcript of Андрей Субботин "Автоматизация локализации...

1

Автоматизациялокализации iOS-приложений

Андрей Субботин

eploko@yandex-team.ru@eploko

2

Зачем?

Как это? Кто?

Что это?

Яндекс. Страшное

Полезное Забавное

3

Зачем нужна локализация?

4

Can't read,won't buy.

52,4% не покупают продукт на чужом языке.

60% — для Франции, Японии и России.

89,3% — если английский знают плохо.

5

http://bit.ly/whylocalize

6

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!?

Andrey Subbotin

eploko@yandex-team.ru

@eploko

Вопросы? :-)