summaryrefslogtreecommitdiff
path: root/lib/tongue/langpack.lua
blob: a2c0e243be9292377e373bbc019103905a9d93cd (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
-- lib/tongue/langpack.lua
--
-- Lua I18N library 'Tongue' - Language Packs
--
-- Copyright 2016 Daniel Silverstone <dsilvers@digital-scurf.org>
--
-- For licence terms, see COPYING
--

--- Language packs comprise zero or more sources of translations for a given
-- "language".
--
-- Packs may be added to at any time.  A pack can have a parent pack which will
-- be used if the pack in question lacks a translation token.  Tongue defines a
-- single default pack (available as tongue.pack.fallback) which always returns
-- a string comprising the token and its arguments flattened as strings.  This
-- is the only part of Tongue which assumes an ability to flatten token
-- arguments as strings, all other parts of tongue preserve token arguments
-- unchanged which allows the passing of complex arguments such as repository
-- objects.
--
-- @module tongue.langpack

local fallback, langpack = nil, {}

--- Fallback language pack.
--
-- The fallback language pack provides a mechanism to ensure that language
-- packs can always expand a translation to some extent.
--
-- @field fallback

--- Try and convert a string to a function.
--
-- This takes a tongue expansion string and tries to convert it to a function
-- which can be used to generate the expansion later.
--
-- @tparam string expn The expansion string
-- @treturn[1] function The resulting function
-- @treturn[2] nil To indicate error
-- @treturn[2] string Error message
local function try_convert(expn)
   return nil, "No converter written"
end

--- Generate a function to localise when a conversion failed.
--
-- This generates a function to report a failed expansion and why
--
-- @tparam string token The token which failed to expand.
-- @tparam string expn The content of that token.
-- @tparam string msg The error message.
-- @treturn function a function which can report that failure
local function report_conversion(token, expn, msg)
   return function(args)
      local nargs = {
	 _pack = args.pack,
	 token = token,
	 expn = expn,
	 msg = msg,
	 args = args
      }
      return args._pack:expand("__TONGUE.FAILEDEXPAND", nargs)
   end
end


--- Tongue Language Pack.
--
-- A Tongue language pack comprises zero or more translations associated with
-- the language (and sub-language) chosen at construction time.  Language packs
-- may have a parent and can be augmented at any time with further
-- translations.
--
-- @type langpack

--- Add a token expansion to a pack.
--
-- This adds the expansion of a token to a tongue language pack.  On addition,
-- if the expansion is a string then it will *NOT* be validated unless the
-- strict argument is set.
--
-- Token names are automatically uppercased in the ASCII charset.
--
-- @tparam string token The token to be expanded
-- @tparam string|function expansion The expansion string (or function)
-- @tparam bool strict Whether to treat a bad expansion string as an error.
-- @treturn bool Whether or not the expansion was successfully added
-- @function add_token

function langpack:add_token(token, expansion, strict)
   if strict and type(expansion) == "string" then
      expansion = try_convert(expansion)
      if not expansion then
	 -- We don't give a reason, we just fail to expand/add
	 return false
      end
   end
   self.entries[token:upper()] = expansion
end

--- Expand a token and arguments into a message.
--
-- This expands the given token and arguments into a full message.  This will
-- always succeed unless something has gone crazywrong with the internals of
-- tongue or the language packs.
--
-- Passed-in tokens are always uppercased before expansion.
--
-- @tparam string token The token to be expanded
-- @tparam table args Arguments to the token expansion
-- @treturn string The expanded result
-- @function expand
function langpack:expand(token, args)
   local pack = self
   if not args._pack then args._pack = pack end
   local ok, expn = pcall(function()
	 if not pack.entries[token] then
	    -- expand upward toward the parent
	    return pack.parent:expand(token, args)
	 end
	 if type(pack.entries[token]) == "string" then
	    -- Attempt to convert the string form into a function
	    local fn, msg = try_convert(pack.entries[token])
	    if not fn then
	       -- Failed to convert, so generate an error report expn. instead
	       fn = report_conversion(token, pack.entries[token], msg)
	    end
	    pack.entries[token] = fn
	 end
	 return pack.entries[token](args)
   end)
   if ok then
      return expn
   end
   -- Failed to expand for whatever reason, we need to report this:
   return self:expand("__TONGUE.INTERNALERROR",
		      { token=token, args=args, err=expn })
end


-- set up the language pack metatable
local langpack_mt = {
   __index = langpack
}

--- Create a language pack.
--
-- This creates a tongue language pack.  Once the pack is created it can be
-- populated in various ways by language loaders.  The passed in language
-- is used by other parts of Tongue (e.g. the message resolver) to manage
-- language packs.
--
-- @tparam string language The language name (e.g. 'en')
-- @tparam ?string sublang  The sub-language name (or nil if unwanted) e.g. "GB"
-- @tparam ?langpack parent The parent langauge pack (if nil, Tongue will use the fallback)
-- @treturn langpack The newly created language pack
-- @function create
local function createpack(language, sublang, parent)
   local retpack = {}
   retpack.language = language
   retpack.sublang = sublang
   retpack.parent = parent or fallback
   retpack.entries = {}
   retpack.lang = sublang and language .. "_" .. sublang or language
   return setmetatable(retpack, langpack_mt)
end

-- Finally, populate the fallback pack with some cleverness
fallback = createpack()

local function flatten(t)
   if t._VISITED then return "???" end
   t._VISITED=t
   local ret = {}
   for k, v in pairs(t) do
      if type(k) == "string" and string.sub(k, 1, 1) ~= "_" then
	 if type(v) == "table" then
	    v = flatten(v)
	 elseif type(v) == "string" then
	    v = ("%q"):format(v)
	 else
	    v = tostring(v)
	 end
	 ret[#ret+1] = k .. "=" .. v
      end
   end
   t._VISITED=nil
   return "{" .. table.concat(ret,",") .. "}"
end

local function fallback_entries_index(t, name)
   local f = function(args)
      return "!!!" .. name .. "!!!" .. flatten(args)
   end
   t[name] = f
   return f
end

setmetatable(fallback.entries, {__index=fallback_entries_index})

fallback.entries["__TONGUE.INTERNALERROR"] = function(args)
   -- args.token
   -- args.err
   -- args.args
   return table.concat({
	 "!!!Internal error while processing", tostring(args.token),
	 "-", tostring(args.err), "-", flatten(args.args)}, " ")
end

fallback.entries["__TONGUE.FAILEDEXPAND"] = function(args)
   -- args.token
   -- args.expn
   -- args.msg
   -- args.args
   return table.concat({
	 "!!!Failed to expand", tostring(args.token), "-", tostring(args.expn),
	 "-", tostring(args.msg), "-", flatten(args.args)}, " ")
end

return {
   create = createpack,
   fallback = fallback
}