Локализация приложений на Форте? Это просто!

Как-то давно для наших ребят на производстве я сделал программку. Она помогала им тестировать и калибровать наше устройство. Ничего особенного, обычная рутина.
А недавно выяснилось, что «наши ребята на производстве» теперь есть и в Америке, и им тоже нужна моя поделка. Дать-то мы им дали, но вот беда, я писал на Форте и по-русски, а от кириллицы (или от кракозябров) на экране, американские партнеры немного обалдевают. Им трудно понять, как это комп может что-то им сообщать… «не по-английски».
Надо переводить… но как?

Сунулся в исходники, мол там все переведу… но тогда наши ребята обидятся, если прога будет ругаться по-английский. Делать две версии, на русском и на английском? А ежели «наши» заведутся в Европе? Дурацкая идея.

Сообразив, что я не первый, кто столкнулся с такой проблемой, спросил совета у Goooogle. Первая же ссылка выдала на гора чудесный gettext и «иже с ним». Но радость была недолгой, так как почитав и посмотрев, я пришел к выводу, что эта либа поддерживает все что угодно, только не Форт.

Вернулся опять к своему «разбитому корыту». Проблема превратилась в вялотекущий процесс…
А тут мне попался перевод книжки (статьи?) CHARLES H.MOORE PROGRAMMING A PROBLEM-ORIENTED-LANGUAGE на форуме gudleifr.forum2x2.ru/t3-topic#222
Нет, саму статью-то я уже видел, но все не досуг было прочитать заметки 50-ти летней давности, а тут как-то заглотил… И знаете, помогло!
Главное, что я вынес оттуда — девиз «Делай просто, делай сам!».
Ну сам, так сам. Чем я хуже? «Тварь я дрожащая или право имею?»

Начал с малого, переопределил слово ." так:
: ." POSTPONE ." ; IMMEDIATE 

Теперь все слова которые станут компилироваться после этого, будут использовать не системное слово, а «моё»… которое вызовет системное. Получилось «масло масленое».
Так, да не так. Теперь у меня есть «ручка» за которую я могу ухватиться.

Расширяю функционал моего слова:
: ."  
    >IN @                     \ узнать текущие смещение от начала входного буфера  
       [CHAR] " PARSE TYPE CR \ из буфера взять строку, ограниченную " и напечатать её
    >IN !                     \ вернуть указатель на место 
    POSTPONE ." ; IMMEDIATE   \ вызвать штатное слово, но потом

Запускаю свою прогу (предварив ее вызовом файла с «моим» словом), и, о чудо, на экране пробежали все строки, что в моей проге были определены через .". Программа осталась рабочей (только не очень опрятной).
Таким образом, с лету, без ковыряния в исходниках (а там десятки файлов), я выудил из них строки сообщений (не все, но большинство). Это круто!
«Ай да Форт, ай да сукин сын!»,- как сказал бы Пушкин.

На радостях переопределил и слово S":
: S"  
    >IN @                     \ узнать текущие смещение от начала входного буфера  
       [CHAR] " PARSE TYPE CR \ из буфера взять строку, ограниченную " и напечатать её
    >IN !                     \ вернуть указатель на место 
    POSTPONE S" ; IMMEDIATE   \ вызвать штатное слово, но потом

Получилось как-то не очень… не, все работает, но дважды писать одно и то же… это моветон. Поэтому общую часть выделил в отдельное слово и получилось так:
: [msg] (  --)
    >in @ 
       [CHAR] " PARSE TYPE CR
    >in !    
    ;
: ." \ подмена печати 
    [msg]
    POSTPONE ." 
    ; IMMEDIATE
: S" \ подмена печати 
    [msg]
    POSTPONE S" 
    ; IMMEDIATE

Так уже лучше, но видеть строки сообщений только на экране маловато, лучше вывести их в файл.
Создал файл:
0 VALUE msgf
S" msg-new.txt" R/W CREATE-FILE THROW TO msgf

И сделал слово для записи строки в этот файл:
: typeF ( adr u --) \ записать в файл
    msgf WRITE-LINE THROW 
    ;

Теперь осталось заменить в [msg] слово TYPE на typeF и вытащенные строчки потекли в файл. Красота!
Вот только толку от этого ровно 0 целых, шиш десятых.

Пришло время подумать, а как собственно сделать перевод, как заставить программу выдавать сообщения на нужном языке.
gettext использует строку сообщения как идентификатор, по которому в файле специального формата ищется соответствующий перевод.
Но:
  1. Сравнивать строки посимвольно достаточно накладно.
  2. Оригинальные строки желательно иметь на английском, он меньше зависит от кодировок.

Получается, что мне все таки придется редактировать исходники, переводить там все строки с русского на английский, чтоб потом перевести на их русский… Звучит как бред.

А что если применить хеширование? Слово красивое, модное и непонятное.
Берем строку, обрабатываем хеш-функцией, получаем хеш и используем его как идентификатор строки (метка). Следовательно хешем должно быть число (проще сравнивать), а на роль хеш-функции тогда возьмем CRC32. То есть, просто для каждой строки считаем контрольную сумму и храним их вместе: число-метка и строка. Все обработанные строки в таком формате связываем в цепочку, кончик которой оставляем снаружи… что-то я вперед забегаю, постараюсь быть последовательным.

Итак, хеширование.
Для начала определил тестовые слова, на которых буду тренироваться:
: msg1 ." Сообщение номер раз." ;
: msg2 S" Второе сообщение." TYPE ;   

Потом сделал слово msgForm, оно принимает строку и её хеш:
: msgForm ( adr u hash-- adr' u') \ оформить сообщение
    hashSz numHex 0 toPad BL SWAP C! 
    hashSz 1+ toPad PAD -    
    PAD SWAP
    ;

и применил его в слове [msg] перед сохранением в файл (а перед этим посчитал контрольную сумму):
: [msg] (  --)
    >in @ 
       [CHAR] " PARSE 2DUP CRC32 msgForm typeF CR
    >in !    
    ;


И теперь, после компиляции тестовых слов, у меня в файле «msg-new.txt» получилось:

63EDD1F7 Сообщение номер раз.
21B87BD6 Второе сообщение.


Видно и число-метку и само сообщение, ему соответствующее. Теперь этот файл можно передавать переводчику и пусть он все русское переведет на нужный язык (главное чтоб первые 8 символов не трогал), а самому заняться хирургией.

Дело в том, что до сих пор моя программа, все ещё использует штатные слова ." и S" которые, честно работают, но ничего не знают о локализации. Оба эти слова каким-то образом сохраняют строки сообщений в программе, обычно прямо в теле определяемого слова, но не факт, так как это зависит от реализации.
Я не стал разбираться кто как это делает, а просто решил сделать свою реализацию этого процесса, т.е. поковыряться внутри определяемых слов, что-то отрезать, а что-то пришить.

  • Пусть в слове храниться не само сообщение, а только его число-метка, хеш.
  • Пусть где-то в памяти будет некий пул (pool), где сложены все строки.

Для начала взял 10Кб из «кучи»:
0 VALUE msgPool 10 1024 * ALLOCATE THROW TO msgPool \ пул сообщений

Адрес начала пула сохранен в переменной msgPool. Переменная здесь удобнее константы, так как по уму пул нужно делать динамического размера, чтоб он рос по мере надобности. Стандартное слово RESIZE умеет корректно изменять размер выделенной памяти, но при этом адрес ее начала может быть изменен. Из этих же соображения, далее буду оперировать не абсолютными адресами, а относительными, это позволит с минимальными усилиями перейти на динамический пул… если понадобится, а пока пусть будет фикс.
Пул можно организовать различными способами, но так или иначе понадобятся слова для работы с ним, например такие:
  • msg! ( adr u hash --) поместить строку в пул;
  • msgFind ( hash — 0|link) найти в пуле по хешу нужную строку, и выдать либо ссылку на неё, либо 0;
  • msg@ ( hash — adr u) выдать строку.
Конкретная их реализация зависит от вкусов автора, я пока не буду отвлекать на это внимание.

Теперь, имея пул сообщений и слова для работы с ним, можно приступить к операции.
В очередной раз переделал слово [msg]:
: [msg] ( --)
       [CHAR] " PARSE 
       2DUP CRC32 \ получить хеш 
       DUP POSTPONE LITERAL \ положить хеш в компилируемое слово
       ['] msg@ COMPILE, \ и добавить его обработку
       DUP msgFind \ проверка на дубль
       IF DROP 2DROP \ есть, ничего не делать 
       ELSE \ нету
            3DUP msg! \ отправить в пул
            msgForm typeF \ отправить в файл
       THEN     
    ;

Оно, собственно, и выполняет всю работу: выделяет сообщения в определяемом слове, компилирует в него хеш (в виде литерала) и слово msg@ для превращения этого хеша в нормальную строку. Кроме этого, это слово, перед добавлением сообщения в пул, проверяет наличие в пуле такого же хеша, и только если там его еще нет, дописывает пул и выводит новое сообщение в файл.
Таким образом в пуле сохраняются и в файл выводятся только уникальные записи. Сделано это не экономии ради, а с умыслом.

Слова-подменыши тоже переписал:
: ." \ подмена печати 
    [msg] ['] TYPE COMPILE, 
    ; IMMEDIATE
: S" \ подмена печати 
    [msg] 
    ; IMMEDIATE
Они стали много проще и уже не используют штатные слова.
Операция успешно завершена и пациент (программа) выжил (работает нормально).

Теперь пора заняться переводом.
От переводчика к нам вернулся переведенный текстовый файл:

63EDD1F7 Message number one.
21B87BD6 Second message.

Назовем его «msg-en.txt» и положим рядом с программой.
Кроме того, все содержимое «msg-new.txt» скопируем в «msg-org.txt».
План действий таков:
  1. Берем файл «msg-org.txt» и заполняем пул его содержимым.
  2. Берем файл «msg-en.txt» и добавляем его содержимое в пул верхним слоем.
  3. Запускаем компиляцию программы.
Если в программе ничего не менялось, то файл «msg-new.txt» будет пустым, так как все сообщения уже были загружены из файла «msg-org.txt». Сама же программа будет говорить по-английски, ведь переведенные строки будут первыми находится в пуле.
Поясню: пул я рассматриваю как нечто, похожее на корзину с бельем, что лежит сверху (добавлено позже), будет рассмотрено первым (отправлено в стиралку).

Итак файлы уже есть, но нет механизма их чтения и размещения в пуле. Сделаем.
\ подгрузка переводов =============
256 CONSTANT szBufLine 
CREATE bufLine szBufLine 2 + ALLOT  ALIGN \ строковый буфер

: hash/ ( adr u -- adr' u' hash) \ отделить хеш от сообщения
    BASE @ >R HEX \ временное переключение в 16-тиричную
        0 0 2SWAP >NUMBER \ перевести хеш в число
        1- SWAP 1+ SWAP \ отрезать разделитель
        2SWAP D>S
    R> BASE !  \ возврат к прежней системе счисления  
    ;

: addLang ( adr u --) \ загрузить сообщения из файла
\ с именем заданными строкой adr u
    R/O OPEN-FILE THROW
    BEGIN DUP bufLine szBufLine ROT
          READ-LINE THROW
    WHILE bufLine SWAP
          DUP hashSz 1+ > if hash/ msg! else 2DROP then
    REPEAT DROP
    CLOSE-FILE THROW
    ;

Слово addLang открывает указаный файл и выгружает из него строки в пул, в этом ему помогает слово hash/ которое переводит символьное представление хеша опять в число.

Теперь осталось совсем чуть-чуть, нужны слова для переключения языка:
VARIABLE langRu  0 langRu !
VARIABLE langEn  0 langEn !
: en ( --) \ переключиться на английский
    langRu @ 0= IF msgLast @ langRu ! THEN
    langEn @ 0= 
    IF S" msg-en.txt"  ['] addLang CATCH 
        IF 2DROP ."  not found." CR 
        ELSE msgLast @ langEn ! THEN
    ELSE langEn @ msgLast !  THEN        
    ;
: ru ( --) \  переключение на оригинальный 
    langRu @ 0= 
    IF   msgLast @ langRu  ! 
    ELSE langRu  @ msgLast !
    THEN
    ;  
\ загрузить оригинальные сообщения, если они есть
S" msg-org.txt" ' addLang CATCH [IF] 2DROP [ELSE] msgLast @ langRu ! [THEN]

Тут последняя строчка пытается аккуратно загрузить файл «msg-org.txt». Если такого не найдется, то этот шаг молча пропускается. Слова-переключатели в основном просто указываю откуда начинать поиск сообщений в пуле. Слово en, кроме того, еще может подгрузить нужный файл с переводом. По его образу и подобию можно сделать слова и для других языков и менять их в процессе работы, как перчатки, только бы не запутаться с указателями.



Как видно на картинке, язык сообщений меняется и его можно выбрать не прерывая сессию и не перезапуская прогу. Подобным образом это работает и в gforth (а не только в spf). Есть надежда, что сработает и в других фортах, так как старался писать в пределах стандарта 94.

Ну и напоследок. Стандарт 94 не определяет поведение слов ." и S" в режиме интерпретации, но spf и gforth позволяют их использовать в этом режиме, правда немного по-разному. Методом тыка я добился одинакового поведения этих слов:
: ." \ подмена печати 
    STATE @
    IF [msg] ['] TYPE COMPILE, 
    ELSE ['] ." EXECUTE THEN
    ; IMMEDIATE
: S" \ подмена печати 
    STATE @
    IF [msg] 
    ELSE ['] S" EXECUTE THEN
    ; IMMEDIATE

То есть в режиме интерпретации слова-подменыши просто вызовут штатные слова, а те будут вести себя так, как им и предписано в системе.
  • +2
  • 23 октября 2020, 15:38
  • iva
  • 1
Файлы в топике: lingva-0.zip

Комментарии (0)

RSS свернуть / развернуть
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.