Что значит "программа правильно написана" ?

    Давайте рассмотрим теперь, как писать "правильные" с точки зрения локализации программы. Основное внимание уделим стандартам.

    (Но и у стандартов есть свои проблемы. Поэтому - немного критики).


    "Правильно" написанная программа с использованием POSIX locale не должна зависить от способа кодирования ("кодировки") символов. Такая программа не должна быть привязана к 7-ми битности ASCII символов, и должна пользоваться стандартными библиотечными (API) функциями localeisalpha(), isupper(), isxxx() и tolower()/toupper() не полагаясь, что Upper=Lower+0x20 и т.д.  Конструкции, подобные этой также недопустимы :

      if (c >= 'A' && c <= 'Z') { ...

Пользуйтесь :

      if (isalpha(c) && isupper(c)) { ... или
      if (isascii(c) && isupper(c))

    Как правило, национальные алфавиты расположены начиная с кода (codepoint) 0x80, поэтому для совместимости со старыми реализациями locale можно объявить все символы как unsigned char, например ключом компилятора (для gcc -funsigned-char) или явно.

    Хорошо написанная программа должна быть польностью 8-бит прозрачна. Например отметка удаленного файла в MS-DOS кодом 0x0E5 - не очень хорошее решение. Еще примеры плохих решений :  знаменитая русская буква "Н" в редакторе GoldEd или русская буква "р" в Norton Commander...

 

    Во-вторых, программа должна явно начинаться с вызова setlocale(LC_ALL,"") (такая форма вызова означает, что всем категориям локализации одновременно будет присвоено значение переменной окружения LANG). До вызова этой функции (или совсем без него) программа не обращает внимания на LANG= и фунции isalpha()... работают в локализации POSIX (С) и с набором символов ASCII. То есть, не бывает "никакой" локализации.

ПРИМЕЧАHИЕ: Во FreeBSD можно вылечить некоторые программы, в которых забыт вызов setlocale() путем задания строки окружения :
$ export ENABLE_STARTUP_LOCALE=""
тогда setlocale(LC_ALL,"") будет вызываться автоматически при старте программ (без их перекомпиляции).

   В Linux libc такого нет (пока?) и по умолчанию всегда включено LANG="C" или "POSIX". Однако можно пересобрать Linux libc, указав другое значение по умолчанию. Но следует отметить, что такое решение будет противоречить стандарту, по которому программа стартует в POSIX (до первого вызова setlocale()).

    Для получения locale-зависимой информации следует пользоватся данными структуры lconv, которые можно получить вызовом функции localeconv() . Для получения детальной информации по категориям локализации (описанным в файле <langinfo.h> ) можно пользовться функцией nl_langinfo(). (Эта функция не входит в POSIX, но входит в XPG, и большинство систем ее имеют).

    Для сравнения строк символов следует пользоваться функциями strcoll() и strxfrm() вместо strcmp().

 

    Для полной поддержки сообщений на родном языке (NLS) весь вывод сообщений пользователю должен происходить c использованием функций NLS и должен быть создан каталог сообщений (message catalog) для данной программы (и данного языка).

    А в заключение, неплохо бы иметь man на разных языках. :-)


Функция setlocale() .

    Основной функцией POSIX locale API является функция selocale(). Данная функция определена в #include файле locale.h как :

char *setlocale(int category, const char * locale);

    Существует три основных формы вызова этой функции :

  1. setlocale(LC_XXX,"language_TERRITORY.Codeset");
  2. setlocale(LC_XXX,"");
  3. str=setlocale(LC_XXX,NULL);

    Первая форма применяется для установки конкретной категории локализации в конкретное значение.

    Вторая форма применяется для установки категории локализации "по умолчанию". В этом случае значение категории локализации берется из переменных окружения.

    Третья форма - это скорее форма GET - позволяет получить текущее значение категории локализации.


Локализация и POSIX.

    Если мы говорим о файле, то в идеологии POSIX, файл - это просто плоская последовательность байтов. Внутреннее содержимое не стандартизовано никак. Поэтому невозможно определить, какую информацию содержит файл, а если он содержит текстовую информацию -- в какой она кодировке. То же самое можно сказать о потоках : stdin/stdout -- это потоки байтов (кодов). Кодировка (т.е. соответствие символ-код (CES)) полностью потеряна и нигде не указывается. И POSIX вовсе не гарантирует, что CES (кодировка) stdin будет совпадать с текущей локализацией, заданной через LANG=.

    Точно тактая же ситуация с терминальным вводом/выводом : кодировка терминала совершенно неизвестна приложению.

   К сожалению, в стандарте POSIX поддержка Charset не имеет полностью идеологически стройной и ясной концепции. Понятие Charset существует только для locale API и тех функций, которые зависят от locale.

    Более того, в "чистом" POSIX вообще невозможно узнать (получить) имя Charset после вызова setlocale(). Единственный способ, узнать Charset текущей locale - это воспользоваться не-POSIX функцией XPG (но определенной в Single UNIXSVID и Unix98) : nl_langinfo(CODESET) (определена в файле <langinfo.h> ). Тогда текущий Сharset можно получить так :

#include <locale.h>
#include <langinfo.h>

...
setlocale(LC_ALL,"");
printf ("Current charset = %s\n",nl_langinfo(CODESET));

     Надо ли говорить, что некоторые UNIX (например FreeBSD) не имеют этих XPG-extensions и не имеют функции nl_langinfo() как таковой вообще. (Что очень странно, поскольку в том же POSIX определена утилита locale c keyword-ом codeset которая "как-то" это имя определяет...) Например, популярная система FreeBSD до сих пор не имеет функции nl_langinfo() (как впрочем не имеет и утилиты locale, увы !).

    Еще один способ определения текущего Charset-а - это разбор переменной окружения LANG= . По стандарту POSIX переменная LANG= задается в форме LANG=language_TERRITORY.Codeset. Например, переменная NLSPATH= (управляющая поведением подсистемы NLS )может иметь "подстановки" :

%L
The value of the LC_MESSAGES category.
%l
The language element from the LC_MESSAGES category.
%t
The territory element from the LC_MESSAGES category.
%c
The codeset element from the LC_MESSAGES category.

    Таким образом, если задать LANG="ru_RU.KOI8-R", то мы получим :

%L
= "ru_RU.KOI8-R"
%l
= "ru"
%t
= "RU"
%c
= "KOI8-R"

    Исходные тексты этих функций открыты и широко доступны.

    Таким образом, установив LANG=ru_RU.KOI8-R мы получим сообщения на русском языке в кодировке KOI8-R, а установив LANG=ru_RU.ISO_8859-5 - в кодировке ISO. На некоторых системах точно так же работает переменная MANPATH=.

    К сожалению, поле Charset - опционально и по стандарту можно использовать сокращенную форму : LANG=ru_RU или даже LANG=ru. (См. localedef ключ -f ). Поэтому, если ваша система это поддерживает, задавайте максимально длинное имя для LANG= , указывая Charset. (получить список можно по locale -a). К сожалению, само имя Charset тоже может различаться.

 

    Довольно значительное число ошибок происходит из за того, что в языке С определен тип переменных char (хотя точнее было бы назвать его : byte). Это во-первых, жестко привязывает нас к 8-ми битным кодировкам. А во-вторых, кодировка не определена. Поэтому, если мы задаем строку (массив char), в которой употребляются символы не ASCII (с кодами >128) : char string[]="Проверка"; -- результат совершенно непредсказуем и непереносим. Еще больше проблем вызывает идея multibyte chars.

    Также вызывает удивление существование (и синтаксическая корректность) типов signed / unsigned char (что такое "отрицательный" символ? Вот unsigned short int -- понятно)... Если вы планируете работу вашей программы в многоязычном окружении, неплохо бы предусмотреть атрибут Сharset у любой строки символов char *. Или полностью переходить на UNICODE (wchar_t) в качестве внутренней кодировки.


Содержание "Locale AS IT IS"


Last change : 02-11-1999