Разбор проектов на Defold 13. Локализация (Localization)

Всем привет :waving_hand:
Продолжаем серию разборов чужого кода с этой страницы: Публичный пример Defold.

Сегодня разбираем пример от britzlЛокализация (Localization).
Попробуйте этот проект в действии: запустите пример.
Исходная папка с проектом: ссылка на github.
Исходная папка с публичными примера Defold на github: ссылка на github.

Обращение к новичкам:

Я предполагаю, что у вас уже установлен Defold, если нет, перейдите по этой ссылке: Добро пожаловать в Defold.
Также, будет плюсом, если вы хотя бы поверхностно знакомы со строительными блоками Defold. Если нет, ознакомиться с основными концепциями Defold можно на официальном сайте — перейдите по этой ссылке.12.

Пример того, как скачать и открыть готовый проект:

Скачиваем архив с примерами проектов из github
Распаковываем скачанный ZIP архив примеров в любую папку во вашему усмотрению.
Переходим в папку examples.
Ищем проект play_animation.
Открываем game.project.

Внимание: скриншоты представлены ниже, это пример скачивания и открытия проекта, в этом примере мы рассматриваем проект с названием play_animation, потому название папок проекта будет отличаться.





Этот проект представляет собой пример реализации системы локализации для игрового движка Defold. Его цель — показать, как можно обрабатывать переведённый текст в играх, переключать языки и отображать соответствующие строки в интерфейсе. Проект является минималистичным, но демонстрирует базовые принципы локализации, которые могут быть расширены для более сложных сценариев.

Рассмотрим файловую структуру этого проекта:

  • В папке fonts размещены файлы, необходимые для отображения пользовательского шрифта в этом проекте.
  • В папке images размещены изображения, которые будут использованы в проекте.
    localization.atlas — это место, куда мы добавим изображения из папки images, которые потом будут использоваться игровыми объектами и их компонентами.
  • localization.collection — коллекция, которая является основной и загрузочной коллекцией этого примера.

    Содержит два игровых объекта. bg — содержит компонент sprite, который берёт изображение из атласа localization.atlas. go — этот игровой объект содержит в себе компоненты sprite, label и script.

Как работает код:

  • Инициализация:
    • Главный скрипт загружает модули localization.translate и localization.translations.
    • Вызывает translate.add_translations(translations), чтобы передать таблицу переводов в M.translations модуля translate.
    • Устанавливает начальный текст метки через translate("GREETING", "Bob").
  • Обновление интерфейса:
    • Каждый кадр (update) главный скрипт отправляет сообщение рендерингу для отображения текущего языка (translate.current_language) и подсказки.
    • При переключении языка (on_input) вызывается translate.change_language(), а затем update_translation() обновляет текст метки.
  • Логика локализации:
    • Модуль translate хранит переводы в M.translations и использует M.current_language для выбора нужного языка.
    • Функция translate (или вызов M(key, ...)) возвращает переведённую строку, подставляя аргументы (например, “Bob”) в %s.
    • Таблица переводов из localization.translations предоставляет строки для GREETING и BYE на двух языках.
Рассмотрим localization.script:

Он реализует простую систему локализации (перевода текста) с возможностью переключения языков по клику:

local translate = require "localization.translate"
local translations = require "localization.translations"

local LANGUAGES = { "en", "se" }

local function update_translation()
	label.set_text("#label", translate("GREETING", "Bob"))
end

function init(self)
	msg.post(".", "acquire_input_focus")
	self.language_index = 0
	translate.add_translations(translations)
	update_translation()
end

function final(self)
	msg.post(".", "release_input_focus")
end

function update(self, dt)
	msg.post("@render:", "draw_text", { text = "Click to change language. Current: " .. translate.current_language, position = vmath.vector3(70, 100, 0) } )
end

function on_input(self, action_id, action)
	if action.released then
		self.language_index = (self.language_index + 1) % #LANGUAGES
		translate.change_language(LANGUAGES[1 + self.language_index])
		update_translation()
	end
end

Подключение модулей (Что такое модуль, что такое require в Defold)

local translate = require "localization.translate"
local translations = require "localization.translations"

Определение доступных языков:

local LANGUAGES = { "en", "se" }

Создаётся таблица LANGUAGES, содержащая коды языков: “en” (английский) и “se” (шведский). Это список языков, между которыми можно переключать.

Функция update_translation:

local function update_translation()
    label.set_text("#label", translate("GREETING", "Bob"))
end
  • Функция обновляет текст компонента с идентификатором #label (текстовый компонент в сцене Defold в localization.collection).
  • translate("GREETING", "Bob") вызывает функцию из модуля translate, которая возвращает переведённую строку для ключа "GREETING", подставляя "Bob" в шаблон перевода.

Функция init:

function init(self)
    msg.post(".", "acquire_input_focus")
    self.language_index = 0
    translate.add_translations(translations)
    update_translation()
end

Это стандартная функция инициализации в Defold, которая вызывается при создании игрового объекта.
msg.post(".", "acquire_input_focus"):

  • Отправляет сообщение текущему объекту (. означает текущий объект), чтобы он получил фокус ввода. Это позволяет объекту обрабатывать пользовательский ввод (например, клики мыши или касания).

self.language_index = 0:

  • Инициализирует переменную language_index в таблице self (контекст игрового объекта) значением 0.
  • language_index отслеживает текущий язык (0 соответствует первому языку в LANGUAGES, т.е. “en”).

translate.add_translations(translations):

  • Регистрирует таблицу переводов из модуля translations в системе локализации. Это делает переводы доступными для функции translate().

update_translation():

  • Вызывает функцию для установки начального текста метки (например, "Hello, Bob!" для английского языка).

Функция final:

function final(self)
    msg.post(".", "release_input_focus")
end

Это стандартная функция Defold, вызываемая при уничтожении игрового объекта.
msg.post(".", "release_input_focus"):

  • Освобождает фокус ввода, чтобы объект больше не обрабатывал пользовательские события. Это хорошая практика для очистки ресурсов.

Функция update:

function update(self, dt)
    msg.post("@render:", "draw_text", { text = "Click to change language. Current: " .. translate.current_language, position = vmath.vector3(70, 100, 0) } )
end

Это стандартная функция Defold, вызываемая каждый кадр.
msg.post("@render:", "draw_text", {...}):

  • Отправляет сообщение системе рендеринга (@render) для отрисовки текста на экране.

{ text = ..., position = vmath.vector3(70, 100, 0) }:

  • text: строка "Click to change language. Current: " + текущий язык (например, "en" или "se"), полученный через translate.current_language.

position: координаты текста на экране (x=70, y=100, z=0).

translate.current_language:

  • Это свойство или функция модуля translate, возвращающая код текущего языка (например, “en”).
  • Результат: на экране отображается текст, например, “Click to change language. Current: en
Рассмотрим translate.lua:
-- create this by hand or generate it from an export from a localization tool of some kind
-- optionally parse it from json
return {
	["en"] = {
		GREETING = "Hello, my name is %s",
		BYE = "Bye",
	},
	["se"] = {
		GREETING = "Hej, jag heter %s",
		BYE = "Hej då",
	}
}

Этот код представляет собой Lua-модуль, который возвращает таблицу с переводами для двух языков: английского (“en”) и шведского (“se”)

Код возвращает Lua-таблицу, которая используется как модуль в Defold. Таблица организована следующим образом:

  • Ключи верхнего уровня — это коды языков: “en” (английский) и “se” (шведский).
  • Для каждого языка есть подтаблица с ключами (GREETING, BYE) и соответствующими строками перевода.

Ключи и значения:

  • GREETING”: строка приветствия, содержащая заполнитель %s для подстановки имени.
    • Для “en”: "Hello, my name is %s" (например, подставив “Bob”, получим “Hello, my name is Bob”).
    • Для “se”: "Hej, jag heter %s" (например, “Hej, jag heter Bob”).
  • BYE”: строка прощания.
    • Для “en”: “Bye”.
    • Для “se”: “Hej då”.
Рассмотрим translations.lua:
local M = {
	default_language = "en",
	current_language = sys.get_sys_info().device_language or sys.get_sys_info().language,
	translations = {},
}

--- Add translations
-- Translations should be in the format:
--
-- {
--   [language] = {
--     [key] = string,
--   }
-- }
-- @param translations
function M.add_translations(translations)
	for language,translations_for_language in pairs(translations) do
		M.translations[language] = M.translations[language] or {} 
		for key,translation in pairs(translations_for_language) do
			M.translations[language][key] = translation
		end
	end
end

--- Change current language
-- @param language
function M.change_language(language)
	M.current_language = language
end

--- Get translation for a specific key, optionally formatting it with
-- additional values
-- @param key The key containing the text to translate
-- @return The translated text
function M.translate(key, ...)
	local t = M.translations[M.current_language] or M.translations[M.default_language]
	if not t or not t[key] then
		return key
	else
		if select("#", ...) > 0 then
			return t[key]:format(...)
		else
			return t[key]
		end
	end
end

-- this will make it possible to call the module table as a function and invoke M.translate()
return setmetatable(M, {
	__call = function(self, key, ...)
		return M.translate(key, ...)
	end
})
local M = {
	default_language = "en",
	current_language = sys.get_sys_info().device_language or sys.get_sys_info().language,
	translations = {},
}

Создаётся локальная таблица M, которая будет возвращена как модуль.
Поля таблицы:

  • default_language = "en": Устанавливает английский язык как язык по умолчанию, если текущий язык недоступен.

  • current_language = sys.get_sys_info().device_language or sys.get_sys_info().language:

    • Устанавливает текущий язык на основе системной информации устройства.
    • sys.get_sys_info().device_language возвращает язык устройства (например, “en”, “se”), если доступно (в Defold это зависит от платформы).
    • Если device_language недоступно (например, на некоторых платформах), используется sys.get_sys_info().language (устаревшее поле, но для совместимости).
    • Если оба значения nil, то current_language останется nil до вызова change_language.
  • translations = {}: Пустая таблица для хранения переводов, куда будут добавляться данные из add_translations.

--- Add translations
-- Translations should be in the format:
--
-- {
--   [language] = {
--     [key] = string,
--   }
-- }
-- @param translations
function M.add_translations(translations)
	for language,translations_for_language in pairs(translations) do
		M.translations[language] = M.translations[language] or {} 
		for key,translation in pairs(translations_for_language) do
			M.translations[language][key] = translation
		end
	end
end
  • Функция добавляет переводы в таблицу M.translations.
  • Ожидаемый формат translations:
{
    ["en"] = { GREETING = "Hello, my name is %s", BYE = "Bye" },
    ["se"] = { GREETING = "Hej, jag heter %s", BYE = "Hej då" }
}
  • Внешний цикл for language,translations_for_language in pairs(translations) перебирает языки (например, “en”, “se”).
  • Для каждого языка создаётся пустая таблица в M.translations[language], если она ещё не существует (M.translations[language] or {}).
  • Внутренний цикл for key,translation in pairs(translations_for_language) добавляет каждую пару ключ-перевод (например, GREETING = "Hello, my name is %s") в таблицу M.translations[language].

После вызова add_translations таблица M.translations заполняется переводами, например:

M.translations = {
    en = { GREETING = "Hello, my name is %s", BYE = "Bye" },
    se = { GREETING = "Hej, jag heter %s", BYE = "Hej då" }
}

Функция change_language

--- Change current language
-- @param language
function M.change_language(language)
M.current_language = language
end

Функция устанавливает текущий язык, сохраняя его в M.current_language.

  • Аргумент language — это строка, например, “en” или “se”.
  • Это используется для выбора нужной таблицы переводов при вызове translate.

Функция translate:

--- Get translation for a specific key, optionally formatting it with
-- additional values
-- @param key The key containing the text to translate
-- @return The translated text
function M.translate(key, ...)
	local t = M.translations[M.current_language] or M.translations[M.default_language]
	if not t or not t[key] then
		return key
	else
		if select("#", ...) > 0 then
			return t[key]:format(...)
		else
			return t[key]
		end
	end
end
  • Функция возвращает переведённую строку для указанного ключа key на текущем языке, с возможностью форматирования строки с дополнительными аргументами (…).
  • Шаги выполнения:
  1. Выбор таблицы переводов:
    local t = M.translations[M.current_language] or M.translations[M.default_language]:
    • Пытается получить таблицу переводов для текущего языка (M.current_language).
    • Если такой таблицы нет (например, язык не поддерживается), используется таблица для языка по умолчанию (M.default_language, т.е. “en”).
  2. Проверка наличия перевода:
  • if not t or not t[key] then return key:
    • Если таблица переводов отсутствует или ключ key не найден, возвращается сам ключ (это защита от ошибок, чтобы вместо ошибки отобразить, например, “GREETING”).
  1. Форматирование строки:
    if select("#", ...) > 0 then:
    • select("#", ...) возвращает количество дополнительных аргументов, переданных в функцию.
    • Если аргументы есть (например, “Bob”), вызывается t[key]:format(...), который форматирует строку с использованием string.format.
      Пример: если t[key] = "Hello, my name is %s" и аргумент "Bob", то результат — "Hello, my name is Bob".
      else return t[key]:
    • Если аргументов нет, возвращается строка перевода без изменений (например, “Bye”).
M.translations = { en = { GREETING = "Hello, %s" }, se = { GREETING = "Hej, %s" } }
M.current_language = "en"
print(M.translate("GREETING", "Bob")) -- Вывод: "Hello, Bob"
M.current_language = "se"
print(M.translate("GREETING", "Bob")) -- Вывод: "Hej, Bob"
print(M.translate("BYE")) -- Вывод: "Hej då" (без форматирования)
print(M.translate("UNKNOWN")) -- Вывод: "UNKNOWN" (ключ не найден)

Метатаблица для вызова модуля как функции:

return setmetatable(M, {
	__call = function(self, key, ...)
		return M.translate(key, ...)
	end
})

Модуль возвращается с установленной метатаблицей, которая позволяет вызывать таблицу M как функцию.

  • Метаметод __call перехватывает вызовы вида M(key, ...) и перенаправляет их в M.translate(key, ...).
    Это делает синтаксис более удобным. Вместо:
M.translate("GREETING", "Bob")

можно писать:

M("GREETING", "Bob")

Например:

local translate = require "localization.translate"
print(translate("GREETING", "Bob")) -- То же, что translate.translate("GREETING", "Bob")
Исправление бага:

Если во время игры фоновое изображение не отображается, то установите z-позицию игрового объекта bg на -0.5:

Надеюсь, кому-нибудь этот материал будет полезен.
Всем спасибо за внимание :light_blue_heart:

Другие разборы:
  1. Разбор проектов на Defold 1. Tilemap Collisions [tilemap]
  2. Разбор проектов на Defold 2. Параллакс [parallax]
  3. Разбор проектов на Defold 3. Движение игровых объектов [animation, movement, input]
  4. Разбор проектов на Defold 4. Воспроизвести анимацию [animation, movement, input]
  5. Разбор проектов на Defold 5. Меню и игра. Прокси-коллекции [proxy-collection, gameloop, collection, gui]
  6. Разбор проектов на Defold 6. Пауза [пауза]
  7. Разбор проектов на Defold 7. Простая кнопка [простая кнопка]
  8. Разбор проектов на Defold 8. Фабрики и свойства [фабрики и свойства]
  9. Разбор проектов на Defold 9. Селектор уровня [селектор уровня].
  10. Разбор проектов на Defold 10. Боец комбо [ввод и анимация]
  11. Разбор проектов на Defold 11. Coin Magnet (Магнит монет) [фабрика]
  12. Разбор проектов на Defold 12. Simple Ai (простой ИИ) [ИИ][AI]