а) мне понравилась идея Александра с локализацией, типа
type TLocString = record Key, Value: string; end;и далее:
var SAskDelete = (Key: 'AskDelete'; Value: 'Удалить?');и далее:
if Ask(SAskDelete.Value) thenС одной стороны это удобно, я прям в коде вижу:
- и переменную
- и Key
- и Value по-умолчанию
Но:
- не очень удобно объявлять такие переменные
- не очень удобно (и не наглядно) писать ".Value"
Ссылка на идею: (18-ть лет с Delphi - Что ещё умеет наш фреймворк?)
б) у нас используется такой вариант: все строковые переменные объявлены как обычно:
var SAskDelete: string;инициализируются они (и реагируют на смену языка) в спец. примерно так:
procedure InitResStrings; begin SAskDelete := ResGet('AskDelete', 'Удалить?'); end;и конечно эта процедура подписывается на обновления
initialization ResReg(@InitResStrings);
Такой вариант проще для адаптации существующего кода, но постоянно писать процедуры инициализации - тоже не комильфо. Много кода, который можно "шаблонизировать".
Собственно это у нас и сделано, и я хочу об этом написать отдельно... с примерами и может быть даже с готовым решением.
А пока у меня вот какие мысли в голове. Во-первых, мы наконец-то перешли с Delphi 7 на Delphi 2010 (ну а значит и на новые версии делфей перебраться не проблема). Поэтому можем использовать все нововведения. Я думал, может как-то прикрутить атрибуты. Например так:
type TMyForm = class(TBaseForm) public [ResString('AskSelete', 'Удалить?')] SAskSelete: string; end;
т.е. я помечаю поле как "ресурсная строка" с ключом и значением по умолчанию.
Тут тоже не всё так красиво. В частности, открыт доступ к полю на запись. Если объявить свойство - то лишняя строчка кода, которую уже не хочется руками набирать - опять повод для кодогенерации.
В общем я по разному крутил. В идеале я хочу получить следующее:
- переменная (или функция, или свойство), которая возвращает ресурсную (локализуемую) строку должна быть только для чтения (записывать могут только спец. методы в момент инициализации/смены языка)
- обращение к переменной в коде должно быть прозрачным
- желательна привязка к контексту (у нас очень много форм, есть вероятность что в двух разных модулях будет переменная с одним и тем же именем)
- желательно ограничить область видимости переменной, например, как поле класса - доступ к переменной возможен только от имени экземпляра класса и при смене языка не надо инициализировать ВСЕ переменные, а только у созданных экземпляров
<эти два пункта у нас реализованы, но с ними можно поспорить>
- возможность по исходнику (либо из приложения в RunTime) сгенерировать файл локализации с языком по-умолчанию (для последующего его перевода).
И есть ещё момент, который может быть не принципиальным, но мы его реализуем. Склонение числительных. Например, в зависимости от кол-ва выделенных строк можно сформировать строку:
0 = 'Нет выделенных строк'
1 = 'Выделена 1 строка'
2 = 'Выделено 2 строки'
5 = 'Выделено 5 строк'
и т.п.
Причём в русском языке есть три формы (строк, строка, строки), а в английском - их две (row, rows).
Вот тут мы в коде пишем что-то типа такого:
if Ask(SSelectedRows[Cnt] + '. ' + SAskDelete) thenТ.е. результат строки зависит от входного числового параметра.
(А есть ещё денежные суммы с рублями и копейками /долларами центами).
Сейчас у нас это всё реализовано тупо, выглядит примерно так:
[Russian] SAskDelete = 'Удалить?' SSelectedRows = ('Выделено %d строк', 'Выделена %d строка', 'Выделено %d строк'); [English] SAskDelete = 'Delete?' SSelectedRows = ('Selected %d rows', 'Selected %d row', 'Selected %d rows');и далее генерится код. И есть функция, которая по числительному определяет форму:
- для русского языка сложнее: (0, 5, 11, 100, нет, много) строк, (1, 21) строка, (2, 3) строки;
- для английского проще: 0 rows, 1 row, >1 rows.
И сейчас я рассматриваю вариант создания некоего DSL, потому что он тут бы подошёл. Нативно для пользователя описать, мол есть строка SSelectedRows, она зависит от числового параметра, и в зависимости от склонения числительного, надо вернуть разные варианты. Или вот зависит от ещё чего-нибудь, например от двух числительных (такое тоже бывает, приходится переформулировать сообщения).
19 коммент.:
>>type
>> TMyForm = class(TBaseForm)
>> public
>> [ResString('AskSelete', 'Удалить?')]
>> SAskSelete: string;
>> end;
Если написали "Удаить" вместо "Удалить".
Поняли потом, заказчик нашел.
Дальше как?
Извините, синтаксис конструкций яп проверяет компилятор.
Кто проверяет синтаксис естественного языка в коде?
Вообще, НЕ дело программиста выдумывать тексты сообщений.
И тексты ниже это явно доказывают.
>>SAskDelete = 'Удалить?'
>>SSelectedRows = ('Выделено %d строк', 'Выделена %d строка', 'Выделено %d строк');
> Если написали "Удаить" вместо "Удалить".
> Поняли потом, заказчик нашел.
> Дальше как?
а) можно как обычно, fix and build. И не говорите, что это плохо, resourcestring не я придумал. И dfm, кстати, тоже хранит Caption'ы и прочие строки. (Delphi way?)
б) в контексте разговора о локализации - достаточно поправить в локализовочном файле. И приложение пересобирать не надо.
> Вообще, НЕ дело программиста выдумывать тексты сообщений.
а) с одной стороны да, согласен. Если бы все использовали UML, то варианты текстов логичнее хранить там же, рисуя варианты ветвления.
б) с другой стороны, Всеволод, давайте не будем идиализировать. Программист реализует бизнес-логику, кому, как не программисту, сформулировать сообщение пользователю в конкретной точке программы? Проектировщику? Но у нас, к сожалению, не используется UML (как у Александра Люлина). Однако строки хранятся в отдельном файле, имя которого совпадает с pas-файлом.
Простейшее и общепринятое решение удовлетворяет приведенным требованиям. А именно:
Берется файл со строками вида
SAskDelete = 'Удалить?'
В коде пишется что-то вроде:
Ask(LStrs.get('SAskDelete'));
Вот этот LStrs и лезет в нужный файл за указанной строкой.
Можно даже так:
Ask(LStrs['SAskDelete']);
- записать туда что-то не получится
- обращение вполне прозрачно. Нормальные IDE могут даже показывать 'Удалить?' вместо LStrs['SAskDelete'].
- привязка к контексту возможна. Хоть для каждой формы свой файл.
- ограничить область видимости переменной LStr можно как угодно
- даже по исходнику можно сгенерировать файл локализации с языком по-умолчанию, если условиться в качестве идентификатора строки брать её значение по умолчанию
Склонение тоже можно прикрутить. Лучше универсальный склонятор написать. Тогда можно будет писать как-то так:
"Выделено %d %строка%"
Изменение всяких caption и text в вцловских компонентах также не составит труда.
> Кто проверяет синтаксис естественного языка в коде?
IDE. Давно уже. Синтаксис ЯП, кстати, тоже.
А мне нравится идея с атрибутами. Мне вообще атрибуты нравятся.
Анонимный
> Простейшее и общепринятое решение..
Замечательное решение! Без сарказма. Но лично мне в нём не нравится то, что я могу допустить опечатку внутри кавычек и написать:
Ask(LStrs.get('SAskDetele'));
и компилятор это скушает. Конечно, можно сказать так: в этом случае надо генерить Exception в RunTime, который должен словить тестировщик. Но это не в моём вкусе.
Александр
Мне тоже нравится. Но пока получается не очень практично...
Всем
Кстати вот парочка ссылок, если кто-то задаётся вопросами локализации, будет полезно:
WebDelphi - 23 решения для локализации и интернационализации приложений
GunSmoker - Локализация проектов Delphi - родной способ (ITE - Integrated Translation Environment)
> Но:
> - не очень удобно объявлять такие переменные
> - не очень удобно (и не наглядно) писать ".Value"
Это-то как раз легко обходится с помощью своего инициализатор и operator overload.
Пример тут: https://gist.github.com/tdelphi/6833017
Кстати про перегрузку операторов я запамятовал. Спасибо за мыслю :)
"Замечательное решение! Без сарказма. Но лично мне в нём не нравится то, что я могу допустить опечатку внутри кавычек и написать"
Ну так используйте enum-ы:
type
TStringKey = (..., sAskDetele, ...); // Defines enumeration range
var
LStrs: array[TStringKey] of string = (..., 'Удалить?', ...);
Вот я рад, что про перечислители тоже вспомнили. Но и у них в данном случае есть минус. Когда строк очень много, то легко промахнуться, инициализируя массив в var секции... ну и с числовыми константами мне не всё нравится...
//Надо всё-таки мне не полениться, да и состряпать заметку о плюсах и минусах разных подходов…
"Когда строк очень много, то легко промахнуться, инициализируя массив в var секции..."
Думаю, вообще не стоит его руками трогать. Пусть он живет в отдельном юните, который автоматически будет генерировать специальный скрипт на основе, к примеру, таблички в БД.
Вот-вот. Снова возвращаемся к кодогенерации...
На самом деле я рассматриваю два варианта:
1) без кодогенерации, но чтобы было максимально просто и удобно объявлять строки и использовать их. Думал, что на атрибутах можно будет получить желаемое. Пока не получилось.
2) с кодогенераций - реализация уже не имеет принципиального значения, главное чтобы из кода было удобно использовать.
вот хочу что-то типа такого:
http://habrahabr.ru/post/190556/
сейчас у меня похоже, но без dll-ки..
Не вижу проблемы в современной делфи сделать один-в-один как там. dll-ку заменить на bpl, заюзать движок лайвбиндингов, пометить атрибутами переводимые свойства. Плюс там в коментах автор пишет, что таки использует кодогенерацию.
Можно так - создать отдельный модуль с константами:
const
msg_NoErr = 0;
....
msg_err_UnableToInitServer = 1000;
...
Так мы определили все возможные сообщения.
Надо добавить еще функцию
Описать простую функцию:
function GetLocSrt(TextCode: integer; Lng: string = "ru_RU"): string
begin
//где можно описать любой алгоритм получения строки под нужный язык.
//можно в лоб, описать case, но для каждого языка будет свой блок case, что не очень удобно.
case TextCode of
msg_NoErr: result := 'Все ок';
msg_err_UnableToInitServer: result := 'что-то не так';
end;
// Поэтому куда лучше, получать данные из файла или какого-нибудь класса, который можно предварительно инициализировать данными из файла.
// В общем как угодно.
end
Теперь в коде можно получить текст сообщения, просто, наглядно и универсально:
GetLocSrt(msg_NoErr)
Все прелести компилятора в нашем распоряжении, и полный список сообщений "msg_" и отдельно список ошибок "msg_err_" и проверка синтаксиса.
Отредактировал:
Можно так - создать отдельный модуль с константами:
const
msg_NoErr = 0;
....
msg_err_UnableToInitServer = 1000;
...
Тем самым, мы определили все возможные сообщения.
Еще потребуется добавить функцию получения строки:
function GetLocSrt(TextCode: integer; Lng: string = "ru_RU"): string
begin
//где можно описать любой алгоритм получения строки под нужный язык.
//хоть в лоб, описать case, но для каждого языка будет свой блок case, что не очень удобно.
case TextCode of
msg_NoErr: result := 'Все ок';
msg_err_UnableToInitServer: result := 'что-то не так';
end;
// Поэтому, куда лучше, получать данные из файла или какого-нибудь объекта, предварительно инициализированного данными из файла.
// В общем как угодно.
end
Теперь, в коде можно получить текст сообщения, просто, наглядно и универсально:
GetLocSrt(msg_NoErr)
Все прелести компилятора в нашем распоряжении, и полный список сообщений "msg_" и отдельно список ошибок "msg_err_" и проверка синтаксиса.
Числовые константы в коде - да. Но тогда в файле ресурсов будут "магические" числа.
Я же хочу, чтобы и в коде я ссылался на строку через некую мнемоническую ссылку, и в файле ресурсов строка описывалась такой же ссылкой. Это чисто для удобства и наглядности не только компилируемого кода, но и самого файла (который можно отдать переводчику, к примеру).
На самом деле оно у меня уже есть, и в коде и в файлах ресурсов строки описываются через строки (а не числа). Я как-нибудь напишу более подробно, как и почему я к такому пришёл.
Ну так не проблема :)
msg_NoErr = 0 можно заменить на msg_NoErr = "msg_NoErr".
Или как-то так: msg_NoErr = "0:msg_NoErr", чтобы код тоже хранить.
В файле можно прописать соответствия на всю строку целиком "0:msg_NoErr" или на часть строки "msg_NoErr", если нет потребности менять числовой код сообщения из файла.
GetText из Unix среды использует подход текстовых ключей. Там идентификатор - фраза на базовом языке (обычно английский, но м.б. и другой), ему сопоставляются переводы. Если перевода не найдено, возвращается строка-ключ.
MsgBox(Transl('Hello world'));
и никто не мешает вынести их в константы
const
SHello = 'Hello world';
MsgBox(Transl(SHello));
Плюсы - всегда есть хоть какой-то перевод (зашит в код), из кода видно фразу, возможность смены перевода на лету. Есть также движки по автоматическому переводу компонентов. Минусы - поиск через сравнение строк, а также проблемы, если сама ключевая фраза меняется (придется менять все файлы перевода). Генерацию списка также придется делать самим, хотя это и не сложно, а в случае выделенного модуля с константами так вообще тривиально.
А вот Android пошел по пути строковых идентификаторов
MsgBox(Transl('str_hello_world'))
при этом ищется строка в текущем переводе, а если ее нет - то в дефолтном.
Что касается множественных чисел (plurals), это должно реализовываться внутри функции перевода на основе специальных меток, типа подстановки Format
const
SDelQuery = 'Hello, %s! Really delete {plural(record|,s,s)}? You'll have {plural(record|,s,s)} left.';
MsgBox(TranslPlural(SDelQuery, [SelRecs, Count-SelRecs], [UserName]));
// TranslPlural сначала запоминает исходную строку в качестве ID, потом получает её перевод, затем выполняет поиск подстановок множ. форм, заменяет их на основе параметров на соотв. форму, а затем уже переправляет в Format для подстановки остальных значений.
А перевод на русский тогда будет выглядеть так
SDelQuery = 'Привет, %s! Точно удалить {plural(запис|ь,и,ей)}? Останется {plural(запис|ь,и,ей)}.';
Правила же склонения множественных чисел стандартизованы и доступны открыто.
Причем правила склонения включают также алгоритмическое выражение по преобразованию любого числа в одну из форм.
К примеру, в английском plural(21) <> plural(1), но = plural(11), тогда как в русском plural(21) = plural(1) и <> plural(11).
Обвязка из доков Андроид
https://developer.android.com/guide/topics/resources/string-resource.html#Plurals
Правда, они задают целую строку для каждой формы, что вроде бы расточительно, но более универсально.
Вот стандарт на множ. формы от GetText
http://docs.translatehouse.org/projects/localization-guide/en/latest/l10n/pluralforms.html
Здесь они просто задают кол-во форм, а формула определяет маппинг числа на одну из них.
http://www.gnu.org/software/gettext/manual/gettext.html#Plural-forms
Отправить комментарий