Логин:   Пароль:






Новости
Рассылки
Форум
Поиск


Java
- Апплеты
- Вопрос-ответ
- Классы
- Примеры
- Руководства
- Статьи
- IDE
- Словарь терминов
- Скачать

Мобильная Java
- Игры
- Примеры
- Статьи
- WAP, WML и пр.

JavaScript
- Вопрос-ответ
- Примеры
- Статьи

Веб-мастеринг
- HTML
- CSS
- SSI

Разминка для ума
Проекты
Книги
Ссылки
Программы
Юмор :)




Rambler's Top100
Rambler's Top100

Мобильная Java: СтатьиСпособ локализации мидлетов

Способ локализации мидлетов

Разрабатывая мидлеты, можно писать код, "зашивая" строки сообщений, выводимых на экран, внутрь кода. При этом мидлет сможет общаться с пользователем только на одном языке. Во многих случаях это вполне приемлемо. Если же требуется, чтобы мидлет адаптировался к настройкам устройства и мог выводить сообщения на нескольких языках, то для этого нужно принять дополнительные меры. Как известно, библиотека MIDP не содержит классов, предназначенных для локализации программ, и разработчикам приходится решать эту задачу самостоятельно. В этой статье я описал способ, которым я пользовался при локализации своих мидлетов.

Краткое описание предлагаемого метода

Суть способа, который я использовал, не нова. Строки, выводимые на экран, выносятся во внешний файл. Для сообщений на разных языках создаются разные файлы. В зависимости от языка, на который настроено мобильное устройство, выбирается тот или иной файл, и строки сообщений берутся из него.

Код локализуемой программы

Сначала приведу пример кода программы, которая нуждается в локализации:

import javax.microedition.midlet.MIDlet;
import javax.microedition.lcdui.*;

public class test extends MIDlet  {

  private TextBox textbox = null;
  private final static String HELLO_WORLD = "Hello World!!!";

  public test() {
    textbox = new TextBox(null, HELLO_WORLD, 20, 0);
  }

  public void startApp() {
    Display.getDisplay(this).setCurrent(textbox);
  }

  public void pauseApp() {}

  public void destroyApp(boolean unconditional) {}
}

В примере для простоты предполагается, что для того, чтобы поприветствовать мир на любом языке, вполне достаточно 20 символов. Теперь посмотрим, как потребуется изменить код для того, чтобы он мог выводить сообщения на разных языках.

1) В строковой константе оставляем ключ, по которому в дальнейшем будем идентифицировать сообщение:

private final static String HELLO_WORLD = "Hello";
2)

Получаем ссылку на экземпляр класса, который отвечает за работу со всеми сообщениями, выводимыми на экран. Этот класс будет рассмотрен ниже.

LocalStrings ls = LocalStrings.getInstance();
3)

Далее заменяем все обращения к строковой константе на вызов метода класса LocalStrings:

textbox = new TextBox("", ls.getString(HELLO_WORLD), 20, 0);

В результате внесенных изменений получился следующий класс:

import javax.microedition.midlet.MIDlet;
import javax.microedition.lcdui.*;
import ru.quadrate.localization.LocalStrings;

public class test1 extends MIDlet  {

  private TextBox textbox = null;
  private final static String HELLO_WORLD = "Hello";

  public test1() {
    LocalStrings ls = LocalStrings.getInstance();
    textbox = new TextBox("", ls.getString(HELLO_WORLD), 20, 0);
  }

  public void startApp() {
    Display.getDisplay(this).setCurrent(textbox);
  }

  public void pauseApp() {}

  public void destroyApp(boolean unconditional) {}
}

Это то, что касается изменения кода локализуемой программы. При этом читаемость программы не ухудшается. Конструкторы и сигнатуры методов не изменяются.

Класс LocalStrings

Класс реализует дизайн-паттерн singleton, который гарантирует, что в нашей программе будет только один экземпляр этого класса. Класс несет ответственность за выбор языка для сообщений, считывание содержимого соответствующего файла ресурса, и затем служит посредником между ключами, прописанными в программе, и реальными сообщениями.

package ru.quadrate.localization;

import java.util.*;
import java.io.*;

/**
* <p>Title: LocalStrings</p>
* <p>Description: Class is responsible for
* program messages localization.</p>
* <p>Copyright: Copyright Quadrate (c) 2003</p>
* @author Quadrate
* @version 1.0
*/
public final class LocalStrings {

  private final String EXTENSION = "strings";
  private Hashtable stringsTable = null;
  private boolean isNotFound; // if no resource files found
  private static LocalStrings instance = null;

  /**
  * Private constructor
  */
  private LocalStrings() {
    isNotFound = false;
    loadStrings();
  }

  /**
  * Creates this class instance if it is not created yet and
  * returns the link.
  * @return
  */
  public static LocalStrings getInstance() {
    if (instance == null) {
      instance = new LocalStrings();
    }
    return instance;
  }

  /**
  * Returns real message in some language by the 
  * key string specified.
  * @param key key string
  * @return message string matching the key specified. 
  * If no value
  * has been found for the key returns the key itself.
  * @return
  */
  public String getString(String key) {
    Object result = isNotFound? key: stringsTable.get(key);
    return (result == null)? key: (String) result;
  }

  /**
  * Reads device's locale and loads appropriate messages from
  * resource file. If there is no file for the locale detected
  * loads file with messages in English.
  */
  private void loadStrings() {
    String locale = System.getProperty ("microedition.locale");
    // get two first letters which describe language
    String language = locale.substring(0, 2).toLowerCase();
    InputStream in = null;
    DataInputStream inReader = null;
    try {
      in = openResource(language + "." + EXTENSION);
      if (in == null) 
      { // there is no resource file for this language
        in = openResource("en" + "." + EXTENSION);
        isNotFound = in == null;
      }
      if (!isNotFound) 
      { // we have opened one of the resource files
        inReader = new DataInputStream (in);
        stringsTable = new Hashtable();
        String key;
        String value;
        while (((key = inReader.readUTF()) != null)
        && ((value = inReader.readUTF()) != null)) 
        { // read key and value
          stringsTable.put(key, value);
        }
      }
    } catch (EOFException ex) {
      // end of file reached
    } catch (Exception ex) {
      isNotFound = true;
      stringsTable = null;
    } finally {
      if (inReader != null) {
        try {
          inReader.close();  // close DataInputStream
        } catch (IOException ex) {  }
      }
      if (in != null) {
        try {
          in.close();  // close InputStream
        } catch (IOException ex) {  }
      }
    }
    // if there is no key-value pairs
    if (stringsTable != null && stringsTable.isEmpty()) {
      stringsTable = null;
      isNotFound = true;
    }
  }

  /**
  * Opens resource file by file name.
  * @param fileName
  * @return
  */
  private InputStream openResource (String fileName) {
    return this.getClass().getResourceAsStream(fileName);
  }
}

Как явствует из кода, класс предполагает, что файлы ресурсов, соответствующие разным языкам, имеют определенные имена и расширения. Кроме того, файлы должны располагаться "рядом" с классом, чтобы он мог их найти. Сами файлы должны быть организованы определенным образом. О том, как подготовить файлы ресурсов, пойдет речь ниже.Сейчас можно только сказать, что файлы созданы с помощью специально написанной программы и выведены в файл методом writeUTF класса DataOutputStream. Сделано это для того, чтобы файл можно было прочитать методом readUTF класса DataInputStream. При чтении предполагается также, что пары key/value располагаются друг за другом. Т.е. все сделано для того, чтобы мобильное устройство не производило никакого парсинга или валидации файла ресурса. Его задача - только прочитать файл и заполнить Hashtable.

Следует отметить, что мы не знаем точно, сколько строк будет считано из файла. При попытке считать строку, когда достигнут конец файла, возникает EOFException. Я не смог найти более "цивилизованного" метода анализа достижения конца файла при чтении методом DataInputStream.readUTF(), чем перехват этого exception-a. Если кто нибудь подскажет - буду благодарен.

Имя файла ресурса соответствует языку, строки для которого он содержит. Точнее, аббревиатуре языка, которая используется в наименовании локали. Например, если устройство настроено на локаль de_DE, то будут взяты первые два символа от названия локали (они определяют язык), будет добавлено расширение "strings" и класс будет искать файл ресурса "de.strings". Таким образом для всех локалей, где используется немецкий язык (DE_CH - Switzerland; DE_AT - Austria; DE_DE - Germany), будут показываться сообщения на немецком. Если файл ресурсов для требуемого языка не найден, то будет предпринята попытка найти файл "en.strings" с сообщениями на английком языке. Если ни одного из файлов ресурсов не найдено, то класс будет в качестве сообщений возвращать значение ключей, прописанных в программе. Поэтому следует писать краткие, но более или менее понятные значения ключей. Это может пригодиться какому-нибудь пользователю, имеющему какое-либо "экстремальное" устройство, которое не смогло считать ни один из файлов ресурсов. В этом случае он увидит по крайней мере ключи и сможет догадаться, что ему хочет сказать программа. Например, для игр этого может быть вполне достаточно.

Как подготовить файл ресурса

Создание файла ресурса подразделяется на две задачи:

  • вынесение ключей и самих сообщений в текстовый файл в кодировке UTF-8
  • конвертация полученного файла, приемлемого для чтения и редактирования человеком, в файл, удобный для мидлета.

Сначала создадим файл со строками, пригодный для конвертации. Формат текстового файла я выбрал следующий:

key=value

key - ключевая строка, зашитая в код мидлета
value - сообщение, выводимое на экран, которое соответствует ключу

Каждая строка файла должна содержать только одну пару ключ/значение. Если требуется, чтобы выводимая на экран строка содержала символ перевода строки, то для этого нужно вписать несколько строк подряд с одинаковым ключом. Такие строки будут объединены, и в местах разрыва строк будет вставлен перевод строки. Например:
siteAdr=My website address:
siteAdr=http://quadrate.astware.com

В конечном итоге, строка для ключа "siteAdr" будет иметь вид: "My website address:\nhttp://quadrate.astware.com". Если одинаковые ключи будут располагаться не подряд, то это будет воспринято конвертором как попытка продублировать ключ, а это уже ошибочная ситуация.

Для рассматриваемого примера файл с англоязычными сообщениями будет иметь вид:

Hello=Hello World!!!

А файл с русскоязычными сообщениями будет выглядеть так:

Hello=Здравствуй, мир!!!

Конвертор предполагает, что исходный файл имеет кодировку UTF-8. Работая с файлом в этой кодировке, можно видеть все специфические символы для любого европейского языка, поэтому с редактированием файла проблем быть не должно. Могу подсказать маленькую хитрость, которую я использовал: если вы не владеете тем языком, на который нужно перевести сообщения, и кто-либо из иностранцев согласился сделать это для вас, то создайте файл в кодировке UTF-8, запишите туда исходные сообщения и пошлите этот файл по email с просьбой вписать перевод именно в этот файл. В результате в ответ вы получите, скорее всего то, что нужно. Останется только отредактировать полученный файл. Если послать текст для перевода прямо в теле письма, то наверняка у вас возникнут сложности с кодировкой переведенных сообщений.

Как я уже упоминал, логика работы класса LocalStrings такова, что если для какого-либо ключа соответствующая строка не найдена, то будет возвращен сам данный ключ. Пользуясь этим, в файлы можно не включать те строки, которые совпадают со своими ключами. Это часто происходит для пунктов меню. "Help", "About", "Start", "End", "Pause" и т.д. Эти строки можно удалить по крайней мере из файла ресурса, содержащего сообщения на английском языке, при условии, что они же являются ключами.

Также нужно отметить небольшую особенность в подготовке текстового файла. Обратите внимание на концевые пробелы в строках. Например, если в программе есть форма, на которой есть поле для ввода с label-ом: "Game Field Width: [5 ]". Если вы хотите, чтобы между label-ом и полем ввода был пробел, то лучше всего вписать его в код программы, а не выносить в файл ресурса. Концевые пробелы не видны, и в одном из файлов для какого-нибудь языка вы его обязательно забудете. Я не говорю уже о том, что многие редакторы просто-напросто обрезают концевые пробелы. Но и лишних пробелов тоже вставлять не следует, т.к. они останутся в составе строки и будут показаны на экране.

Конвертор

Будем считать, что вы подготовили файлы с сообщениями в описанном выше формате. Далее нужно просто запустить конвертор, указав в качестве параметров путь к исходному файлу и путь к результирующему файлу. Конвертор работает в соответствии с правилами, приведенными в предыдущем разделе, где описывается, как должен быть отформатирован исходный файл. Проверок на корректность файла в конверторе нет.

package ru.quadrate.localization;

import java.io.*;
import java.util.*;
import java.text.*;

/**
* <p>Title: Helper</p>
* <p>Description: Converts files with messages to 
* display from human-readable format to
* midlet-readable format.</p>
* <p>Copyright: Copyright Quadrate(c) 2003</p>
* @author Quadrate
* @version 1.0
*/
public class Helper {

  private final static String FILE_CHARACTER_SET = "UTF-8";
  private final static String DELIMITER = "=";

  /**
  * No instances
  */
  private Helper() {  }

  public static void main(String[] args) {
    if (args.length != 2) {
      System.out.println("Illegal parameters. 
    Please specify source file and destination file names.");
    } else {
      // Open in and out streams
      File sourceFile = new File(args[0]);
      File destFile = new File(args[1]);
      FileInputStream in = null;
      BufferedReader inReader = null;
      FileOutputStream out = null;
      DataOutputStream outWriter = null;
      try {
        // open source
        in = new FileInputStream(sourceFile);
        in.skip(3);
        inReader = new BufferedReader(new InputStreamReader 
                                   (in, FILE_CHARACTER_SET));
        // open destination
        out = new FileOutputStream(destFile);
        outWriter = new DataOutputStream(out);

        Hashtable stringsTable = new Hashtable();
        readSourceContent(inReader, stringsTable);

        writeContent(outWriter, stringsTable);
      } catch (Exception ex) {
        ex.printStackTrace();
      } finally {
        try {
          if (in != null) {
in.close();
          }
          if (out != null) {
out.close();
          }
        } catch (Exception ex) {
          ex.printStackTrace();
        }
      }
    }
  }

  /**
  * Writes hashtable content into the stream specified.
  * @param outWriter
  * @param stringsTable
  * @throws IOException
  */
  private static void writeContent(DataOutputStream outWriter,
  Hashtable stringsTable) throws IOException {
    for (Enumeration en = stringsTable.keys(); 
                                en.hasMoreElements();) {
      String key = (String) en.nextElement();
      String value = (String) stringsTable.get(key);
      outWriter.writeUTF(key);
      outWriter.writeUTF(value);
    }
  }

  /**
  * Reads and parses content of the source file.
  * @param inReader
  * @param stringsTable
  * @throws IOException
  */
  private static void readSourceContent(BufferedReader inReader,
  Hashtable stringsTable) throws IOException, ParseException {
    String line = null;
    String prevKey = null;
    while ((line = inReader.readLine()) != null) {
      int delimiterPos = line.indexOf(DELIMITER);
      if (delimiterPos != -1) {
        String key = line.substring (0, delimiterPos);
        String value = line.substring (delimiterPos + 1);
        if (stringsTable.containsKey(key)) {
          if (prevKey.equals(key)) {
StringBuffer buf = new StringBuffer((String) 
                              stringsTable.get(prevKey));
buf.append("\n").append(value);
value = buf.toString();
          } else {
throw new ParseException("Duplicate key: " + key, 0);
          }
        }
        prevKey = key;
        stringsTable.put(key, value);
        System.out.println("key: " + key + "; value: " + value);
      }
    }
  }
}

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

После обработки файла, сделанного вручную, мы получим файл ресурса, готовый для восприятия мидлетом. Остается положить его в ту же директорию, где будет лежать LocalStrings.class внутри jar файла.

Проверка работы мидлета

Откомпилируем и соберем мидлет. Предположим, что рядом с LocalStrings.class в jar файле находятся два файла ресурсов en.strings и ru.strings. Теперь при запуске этой программы на реальном устройстве или на эмуляторе, в зависимости от выбранной локали, мы будем видеть сообщения на английском или на русском языке.

Резюме

В завершение описания предложенного метода хотелось бы остановиться на его сильных и слабых сторонах, а также дать несколько советов. Некоторые из тезисов уже были изложены в статье.

Минусы:

  • В мидлете считывание строк из файла ресурса всегда заканчивается Exception-ом.
  • При конвертации текстового файла в файл ресурса происходит skip первых трех байт.
  • Имеет место некоторая избыточность - строки ключей повторяются и в коде программы, и в каждом файле ресурса.
  • Нужно аккуратно обращаться с концевыми пробелами в текстовом файле.
  • На мой взгляд, самое неприятное то, что в процессе работы мидлета все строки висят в памяти независимо от того, нужны они будут в ближайшее время или нет. Например, нет смысла постоянно держать в памяти устройства текст help-a к игре. Подавляющее время работы мидлета этот текст не будет нужен. Я полагаю, что этот недостаток является веским основанием для усовершенствования метода. По крайней мере, необходимо разделить строки на "часто используемые" и "редко используемые", при этом первые должны храниться в оперативной памяти, а вторые - считываться из файла ресурса по мере надобности. Для этого, правда, придется несколько усложнить класс LocalStrings.

Плюсы:

  • Пользователь всегда увидит какие-либо строки и сможет работать с программой. В худшем случае это будут ключи, "зашитые" в коде.
  • Если ключ короткий и совпадает с выводимой строкой, то можно убрать эту пару из файла ресурса.
  • Ключи в виде строк облегчают работу. Не теряется читаемость кода и текстовых файлов. Если доступ сделать, например, через цифровой индекс, то писать код и создавать файлы ресурсов будет намного сложнее.
  • Облегчено получение ссылки на класс LocalStrings. Не требуется изменять сигнатуры методов и конструкторов.

Рекомендации:

  • При работе с иностранными языками, имеющими специфические символы, пользуйтесь текстовыми файлами в кодировке UTF-8. Если кто-то из иностранцев согласится помочь вам с переводом сообщений, пошлите ему файл в кодировке UTF-8, содержащий оригинальные строки. Он впишет туда перевод и пришлет его вам обратно. Это снимет проблему с кодировкой.
  • Для того, чтобы проверить соответствие программного кода и файлов ресурсов, можно временно изменить код класса LocalStrings так, чтобы в случае, если для ключа не найдено значение, то возвращался бы не сам ключ, а какая нибудь заметная строка, например "***". Если вы забыли прописать какой либо ключ в файле ресурса, то при работе программы вы обязательно обнаружите это место.
Автор: Антон Чумак

Warning: mysql_connect() [function.mysql-connect]: Can't connect to local MySQL server through socket '/tmp/mysql.sock' (2) in /pub/home/javaport/javaportal/books/show2b.php on line 11

Warning: mysql_db_query() [function.mysql-db-query]: Can't connect to local MySQL server through socket '/tmp/mysql.sock' (2) in /pub/home/javaport/javaportal/books/show2b.php on line 19

Warning: mysql_db_query() [function.mysql-db-query]: A link to the server could not be established in /pub/home/javaport/javaportal/books/show2b.php on line 19

Warning: mysql_fetch_array(): supplied argument is not a valid MySQL result resource in /pub/home/javaport/javaportal/books/show2b.php on line 30
Узнай о чем ты на самом деле сейчас думаешь тут.


[an error occurred while processing this directive]



Warning: mysql_connect() [function.mysql-connect]: Can't connect to local MySQL server through socket '/tmp/mysql.sock' (2) in /pub/home/javaport/javaportal/news/worldnews.php on line 91

Warning: mysql_db_query() [function.mysql-db-query]: Can't connect to local MySQL server through socket '/tmp/mysql.sock' (2) in /pub/home/javaport/javaportal/news/worldnews.php on line 93

Warning: mysql_db_query() [function.mysql-db-query]: A link to the server could not be established in /pub/home/javaport/javaportal/news/worldnews.php on line 93

Warning: mysql_fetch_array(): supplied argument is not a valid MySQL result resource in /pub/home/javaport/javaportal/news/worldnews.php on line 95