Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 53 additions & 42 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,27 +49,28 @@ Initialize the **Lang** module by calling `lang.init()` with your language confi
```lua
local lang = require("lang.lang")

-- Initialize with language files
-- Basic initialization with language files
lang.init({
{ id = "en", path = "/resources/lang/en.json" },
{ id = "ru", path = "/resources/lang/ru.json" },
{ id = "es", path = "/resources/lang/es.json" },
})
```

You can also force a specific language on initialization:
#### Force Language on Start

You can force a specific language on initialization:

```lua
-- Force a specific language on start
-- Force Spanish language on start
lang.init({
{ id = "en", path = "/resources/lang/en.json" },
{ id = "ru", path = "/resources/lang/ru.json" },
{ id = "es", path = "/resources/lang/es.json" },
}, "es") -- Force Spanish language
}, "es")
```


### Default Language
#### Language Selection Priority

**Defold Lang** selects the language to use in the following priority order:

Expand All @@ -80,16 +81,14 @@ lang.init({

The first language in the configuration array serves as the ultimate fallback. Defold uses the two-character [ISO-639 format](https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes) for language codes ("en", "ru", "es", etc).

The module uses `sys.load_resource` to load the files. Place your files inside your [custom resources folder](https://defold.com/manuals/project-settings/#custom-resources) to ensure they are included in the build.
> **Note:** Place your language files inside your [custom resources folder](https://defold.com/manuals/project-settings/#custom-resources) to ensure they are included in the build.


### Localization Files

**Defold Lang** supports three file formats: **JSON**, **Lua**, and **CSV**. Each format has its own advantages:
**Defold Lang** supports three file formats: **JSON**, **Lua**, and **CSV**.

#### JSON Files
JSON files use a simple key-value structure:

```json
{
"ui_hello_world": "Hello, World!",
Expand All @@ -99,18 +98,7 @@ JSON files use a simple key-value structure:
}
```

Initialize with JSON files:
```lua
lang.init({
{ id = "en", path = "/locales/en.json" },
{ id = "ru", path = "/locales/ru.json" },
{ id = "es", path = "/locales/es.json" },
})
```

#### Lua Files
Lua files return a table with translations:

```lua
-- en.lua
return {
Expand All @@ -121,18 +109,7 @@ return {
}
```

Initialize with Lua files:
```lua
lang.init({
{ id = "en", path = require("locales.en") },
{ id = "ru", path = require("locales.ru") },
{ id = "es", path = require("locales.es") },
})
```

#### CSV Files
CSV files allow multiple languages in a single file. The first column contains keys, and subsequent columns contain translations:

```csv
key,en,ru,es
ui_hello_world,"Hello, World!","Привет, мир!","¡Hola, mundo!"
Expand All @@ -141,34 +118,68 @@ ui_settings,Settings,Настройки,Configuración
ui_exit,Exit,Выход,Salir
```

Initialize with CSV files (specify column names as language IDs):
#### Mixed Format Example
You can mix different file formats in a single configuration:

```lua
lang.init({
{ id = "en", path = "/locales/translations.csv" },
{ id = "ru", path = "/locales/translations.csv" },
{ id = "es", path = "/locales/translations.csv" },
{ id = "en", path = "/resources/lang/en.json" },
{ id = "ru", path = require("resources.lang.ru") },
{ id = "es", path = "/resources/lang/translations.csv" },
})
```

#### Mixed Format Example
You can even mix different file formats:
### Load from Bundle Resources

#### Async Loading with Custom Loaders

For loading from bundle resources (file I/O or HTTP), you can provide a custom loader function:

```lua
-- Custom async loader for bundle resources, HTTP loading, etc.
lang.init({
{ id = "en", path = "/resources/lang/en.json" },
{ id = "ru", path = "/resources/lang/ru.lua" },
{ id = "es", path = "/resources/lang/translations.csv" },
{
id = "en",
path = "/bundle/lang/en.json",
loader = function(path, on_success, on_error)
-- Your custom loading logic here
-- Call on_success(content) with file content string
-- Or on_error(error_message) on failure

-- Example: Bundle resource loading
local app_path = sys.get_application_path()
local full_path = app_path .. path
local f = io.open(full_path, "rb")
if f then
local content = f:read("*a")
f:close()
on_success(content)
else
on_error("File not found: " .. full_path)
end
end
},
})

-- Language switching with callback
lang.set_lang("en", function()
print("Language loaded!")
print(lang.txt("ui_hello"))
druid.on_language_change()
end)
```

**Use cases for bundle resource loading:**
- Bundle resources loading (file I/O or HTTP)
- Platform-specific resource access

## API Reference

### Quick API Reference

```lua
lang.init(available_langs, [lang_on_start])
lang.set_lang(lang_id)
lang.set_lang(lang_id, [on_lang_changed])
lang.get_lang()
lang.get_langs()
lang.set_next_lang()
Expand Down
3 changes: 3 additions & 0 deletions game.project
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,6 @@ dependencies#0 = https://github.com/britzl/deftest/archive/master.zip
[library]
include_dirs = lang

[html5]
splash_image = /media/logo_thumb.png

77 changes: 77 additions & 0 deletions lang/internal/lang_logger.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
---@class lang.logger
---@field trace fun(_, msg: string, data: any)
---@field debug fun(_, msg: string, data: any)
---@field info fun(_, msg: string, data: any)
---@field warn fun(_, msg: string, data: any)
---@field error fun(_, msg: string, data: any)
local M = {}

local EMPTY_FUNCTION = function(_, message, context) end

---@type lang.logger
local empty_logger = {
trace = EMPTY_FUNCTION,
debug = EMPTY_FUNCTION,
info = EMPTY_FUNCTION,
warn = EMPTY_FUNCTION,
error = EMPTY_FUNCTION,
}

---@type lang.logger
local default_logger = {
trace = function(_, msg, data) print("TRACE: " .. msg, M.table_to_string(data)) end,
debug = function(_, msg, data) print("DEBUG: " .. msg, M.table_to_string(data)) end,
info = function(_, msg, data) print("INFO: " .. msg, M.table_to_string(data)) end,
warn = function(_, msg, data) print("WARN: " .. msg, M.table_to_string(data)) end,
error = function(_, msg, data) print("ERROR: " .. msg, M.table_to_string(data)) end
}

local METATABLE = { __index = default_logger }

function M.set_logger(logger)
METATABLE.__index = logger or empty_logger
end


---Converts table to one-line string
---@param t table
---@param depth number?
---@param result string|nil Internal parameter
---@return string, boolean result String representation of table, Is max string length reached
function M.table_to_string(t, depth, result)
if type(t) ~= "table" then
return tostring(t) or "", false
end

depth = depth or 0
result = result or "{"

for key, value in pairs(t) do
if #result > 1 then
result = result .. ", "
end

if type(value) == "table" then
if depth == 0 then
local table_len = 0
for _ in pairs(value) do
table_len = table_len + 1
end
result = result .. key .. ": {... #" .. table_len .. "}"
else
local convert_result, is_limit = M.table_to_string(value, depth - 1, "")
result = result .. key .. ": {" .. convert_result
if is_limit then
break
end
end
else
result = result .. key .. ": " .. tostring(value)
end
end

return result .. "}", false
end


return setmetatable(M, METATABLE)
Loading