tina4ruby 3.10.92 → 3.10.93
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/tina4/frond.rb +30 -3
- data/lib/tina4/localization.rb +55 -8
- data/lib/tina4/orm.rb +1 -1
- data/lib/tina4/rack_app.rb +74 -1
- data/lib/tina4/seeder.rb +7 -0
- data/lib/tina4/template.rb +101 -5
- data/lib/tina4/version.rb +1 -1
- data/lib/tina4.rb +14 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5820ce91766359bcb5365ef8ea729e09641d2477b0b68179ca7c617fc3ce4630
|
|
4
|
+
data.tar.gz: 05c9793be438311072c37ce8570aea4c6322d7b498a2b079007996450ca8006b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ba80ea5428f0354e0c005a0e647f1a1c8982bf2bbc9a69059a86b20d49b9523448f1fec0e28a7e9c459b441987b811e91b50ae77cc2fdafd6100f64fdc34789a
|
|
7
|
+
data.tar.gz: 4eb11c4f9a289f88a49330a763b32a595c7c46715788af3c8ada2d8742e7bd5d35f8d219416e4e49a3e794c4cf2fc23d23032f7b99e1b3ed8536345d3ec4800e
|
data/lib/tina4/frond.rb
CHANGED
|
@@ -655,6 +655,7 @@ module Tina4
|
|
|
655
655
|
def find_outside_quotes(expr, needle)
|
|
656
656
|
in_q = nil
|
|
657
657
|
depth = 0
|
|
658
|
+
bracket_depth = 0
|
|
658
659
|
i = 0
|
|
659
660
|
nlen = needle.length
|
|
660
661
|
while i <= expr.length - nlen
|
|
@@ -676,8 +677,12 @@ module Tina4
|
|
|
676
677
|
depth += 1
|
|
677
678
|
elsif ch == ")"
|
|
678
679
|
depth -= 1
|
|
680
|
+
elsif ch == "["
|
|
681
|
+
bracket_depth += 1
|
|
682
|
+
elsif ch == "]"
|
|
683
|
+
bracket_depth -= 1
|
|
679
684
|
end
|
|
680
|
-
if depth == 0 && expr[i, nlen] == needle
|
|
685
|
+
if depth == 0 && bracket_depth == 0 && expr[i, nlen] == needle
|
|
681
686
|
return i
|
|
682
687
|
end
|
|
683
688
|
i += 1
|
|
@@ -1172,8 +1177,30 @@ module Tina4
|
|
|
1172
1177
|
part = part.strip.gsub(RESOLVE_STRIP_RE, "") # strip quotes from bracket access
|
|
1173
1178
|
if value.is_a?(Hash) || value.is_a?(LoopContext)
|
|
1174
1179
|
value = value[part] || value[part.to_sym]
|
|
1175
|
-
elsif value.is_a?(Array)
|
|
1176
|
-
value
|
|
1180
|
+
elsif value.is_a?(Array)
|
|
1181
|
+
# Slice syntax: value[1:5], value[:10], value[start:end]
|
|
1182
|
+
if part.include?(":") && !(part.start_with?('"') || part.start_with?("'"))
|
|
1183
|
+
slice_parts = part.split(":", 2)
|
|
1184
|
+
s_start = slice_parts[0].strip.empty? ? nil : eval_expr(slice_parts[0].strip, context).to_i
|
|
1185
|
+
s_end = slice_parts[1].strip.empty? ? nil : eval_expr(slice_parts[1].strip, context).to_i
|
|
1186
|
+
if s_start && s_end
|
|
1187
|
+
value = value[s_start...s_end]
|
|
1188
|
+
elsif s_start
|
|
1189
|
+
value = value[s_start..]
|
|
1190
|
+
elsif s_end
|
|
1191
|
+
value = value[0...s_end]
|
|
1192
|
+
else
|
|
1193
|
+
value = value.dup
|
|
1194
|
+
end
|
|
1195
|
+
next
|
|
1196
|
+
end
|
|
1197
|
+
idx = if part =~ DIGIT_RE
|
|
1198
|
+
part.to_i
|
|
1199
|
+
else
|
|
1200
|
+
eval_expr(part, context)
|
|
1201
|
+
end
|
|
1202
|
+
idx = idx.to_i if idx.is_a?(Numeric)
|
|
1203
|
+
value = idx.is_a?(Integer) ? value[idx] : nil
|
|
1177
1204
|
elsif value.respond_to?(part.to_sym)
|
|
1178
1205
|
value = value.send(part.to_sym)
|
|
1179
1206
|
else
|
data/lib/tina4/localization.rb
CHANGED
|
@@ -10,6 +10,12 @@ module Tina4
|
|
|
10
10
|
@translations ||= {}
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
+
# Flat alias map: { locale => { leaf_key => value } }
|
|
14
|
+
# First-wins on conflict — later duplicates are ignored.
|
|
15
|
+
def flat_aliases
|
|
16
|
+
@flat_aliases ||= {}
|
|
17
|
+
end
|
|
18
|
+
|
|
13
19
|
def current_locale
|
|
14
20
|
@current_locale || ENV["TINA4_LOCALE"] || "en"
|
|
15
21
|
end
|
|
@@ -19,8 +25,11 @@ module Tina4
|
|
|
19
25
|
end
|
|
20
26
|
|
|
21
27
|
def load(root_dir = Dir.pwd)
|
|
22
|
-
|
|
23
|
-
|
|
28
|
+
locale_dir_override = ENV["TINA4_LOCALE_DIR"]
|
|
29
|
+
search_dirs = locale_dir_override && !locale_dir_override.empty? ? [locale_dir_override] : LOCALE_DIRS
|
|
30
|
+
|
|
31
|
+
search_dirs.each do |dir|
|
|
32
|
+
locale_dir = File.expand_path(dir, root_dir)
|
|
24
33
|
next unless Dir.exist?(locale_dir)
|
|
25
34
|
|
|
26
35
|
Dir.glob(File.join(locale_dir, "*.json")).each do |file|
|
|
@@ -28,6 +37,8 @@ module Tina4
|
|
|
28
37
|
data = JSON.parse(File.read(file))
|
|
29
38
|
translations[locale] ||= {}
|
|
30
39
|
translations[locale].merge!(data)
|
|
40
|
+
# Build leaf-key aliases from the loaded data
|
|
41
|
+
build_leaf_aliases(locale, data)
|
|
31
42
|
Tina4::Log.debug("Loaded locale: #{locale} from #{file}")
|
|
32
43
|
end
|
|
33
44
|
|
|
@@ -37,8 +48,11 @@ module Tina4
|
|
|
37
48
|
require "yaml"
|
|
38
49
|
locale = File.basename(file, File.extname(file))
|
|
39
50
|
data = YAML.safe_load(File.read(file))
|
|
40
|
-
|
|
41
|
-
|
|
51
|
+
if data.is_a?(Hash)
|
|
52
|
+
translations[locale] ||= {}
|
|
53
|
+
translations[locale].merge!(data)
|
|
54
|
+
build_leaf_aliases(locale, data)
|
|
55
|
+
end
|
|
42
56
|
rescue LoadError
|
|
43
57
|
Tina4::Log.warning("YAML support requires the 'yaml' gem")
|
|
44
58
|
end
|
|
@@ -94,6 +108,13 @@ module Tina4
|
|
|
94
108
|
hash = hash[k]
|
|
95
109
|
end
|
|
96
110
|
hash[keys.last] = value
|
|
111
|
+
|
|
112
|
+
# Register leaf-key alias (first-wins)
|
|
113
|
+
leaf = keys.last
|
|
114
|
+
if value.is_a?(String)
|
|
115
|
+
flat_aliases[locale.to_s] ||= {}
|
|
116
|
+
flat_aliases[locale.to_s][leaf] ||= value
|
|
117
|
+
end
|
|
97
118
|
end
|
|
98
119
|
|
|
99
120
|
def available_locales
|
|
@@ -102,19 +123,45 @@ module Tina4
|
|
|
102
123
|
|
|
103
124
|
private
|
|
104
125
|
|
|
126
|
+
# Recursively walk a nested hash and register leaf-key aliases.
|
|
127
|
+
# First-wins: if a leaf key already exists, it is NOT overwritten.
|
|
128
|
+
def build_leaf_aliases(locale, hash, prefix = nil)
|
|
129
|
+
flat_aliases[locale.to_s] ||= {}
|
|
130
|
+
hash.each do |key, value|
|
|
131
|
+
full_key = prefix ? "#{prefix}.#{key}" : key.to_s
|
|
132
|
+
if value.is_a?(Hash)
|
|
133
|
+
build_leaf_aliases(locale, value, full_key)
|
|
134
|
+
else
|
|
135
|
+
# Store the leaf key as an alias (first-wins)
|
|
136
|
+
flat_aliases[locale.to_s][key.to_s] ||= value
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
105
141
|
def lookup(locale, key)
|
|
106
142
|
keys = key.to_s.split(".")
|
|
107
143
|
result = translations[locale]
|
|
108
144
|
return nil unless result
|
|
109
145
|
|
|
146
|
+
# Try dot-path traversal first
|
|
147
|
+
dot_result = result
|
|
110
148
|
keys.each do |k|
|
|
111
|
-
if
|
|
112
|
-
|
|
149
|
+
if dot_result.is_a?(Hash)
|
|
150
|
+
dot_result = dot_result[k] || dot_result[k.to_sym]
|
|
113
151
|
else
|
|
114
|
-
|
|
152
|
+
dot_result = nil
|
|
153
|
+
break
|
|
115
154
|
end
|
|
116
155
|
end
|
|
117
|
-
|
|
156
|
+
return dot_result if dot_result.is_a?(String)
|
|
157
|
+
|
|
158
|
+
# Fall back to leaf-key alias (only for simple keys without dots)
|
|
159
|
+
if flat_aliases[locale]
|
|
160
|
+
alias_val = flat_aliases[locale][key.to_s]
|
|
161
|
+
return alias_val if alias_val.is_a?(String)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
nil
|
|
118
165
|
end
|
|
119
166
|
end
|
|
120
167
|
end
|
data/lib/tina4/orm.rb
CHANGED
data/lib/tina4/rack_app.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
require "json"
|
|
3
3
|
require "securerandom"
|
|
4
|
+
require "uri"
|
|
4
5
|
|
|
5
6
|
module Tina4
|
|
6
7
|
# Middleware wrapper that tags requests arriving on the AI dev port.
|
|
@@ -168,8 +169,35 @@ module Tina4
|
|
|
168
169
|
|
|
169
170
|
# Secure-by-default: enforce bearer-token auth on write routes
|
|
170
171
|
if route.auth_required
|
|
172
|
+
token = nil
|
|
173
|
+
token_source = nil # :header, :body, :session
|
|
174
|
+
|
|
175
|
+
# Priority 1: Authorization Bearer header
|
|
171
176
|
auth_header = env["HTTP_AUTHORIZATION"] || ""
|
|
172
|
-
|
|
177
|
+
if auth_header =~ /\ABearer\s+(.+)\z/i
|
|
178
|
+
token = Regexp.last_match(1)
|
|
179
|
+
token_source = :header
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Priority 2: formToken from request body (for frond.js saveForm with {{ form_token() }})
|
|
183
|
+
if token.nil?
|
|
184
|
+
body_str = _read_rack_body(env)
|
|
185
|
+
form_token = _extract_form_token(body_str, env)
|
|
186
|
+
if form_token && !form_token.empty?
|
|
187
|
+
token = form_token
|
|
188
|
+
token_source = :body
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Priority 3: Session token (for secured GET routes after login)
|
|
193
|
+
if token.nil?
|
|
194
|
+
session = Tina4::Session.new(env)
|
|
195
|
+
session_token = session.get("token")
|
|
196
|
+
if session_token && !session_token.empty?
|
|
197
|
+
token = session_token
|
|
198
|
+
token_source = :session
|
|
199
|
+
end
|
|
200
|
+
end
|
|
173
201
|
|
|
174
202
|
# API_KEY bypass — matches tina4_python behavior
|
|
175
203
|
api_key = ENV["TINA4_API_KEY"] || ENV["API_KEY"]
|
|
@@ -180,6 +208,11 @@ module Tina4
|
|
|
180
208
|
return [401, { "content-type" => "application/json" }, [JSON.generate({ error: "Unauthorized" })]]
|
|
181
209
|
end
|
|
182
210
|
env["tina4.auth_payload"] = Tina4::Auth.get_payload(token)
|
|
211
|
+
|
|
212
|
+
# When body formToken validates, store a refreshed token for the FreshToken response header
|
|
213
|
+
if token_source == :body
|
|
214
|
+
env["tina4.fresh_token"] = Tina4::Auth.refresh_token(token)
|
|
215
|
+
end
|
|
183
216
|
else
|
|
184
217
|
return [401, { "content-type" => "application/json" }, [JSON.generate({ error: "Unauthorized" })]]
|
|
185
218
|
end
|
|
@@ -231,6 +264,11 @@ module Tina4
|
|
|
231
264
|
# Run global after middleware (block-based + class-based after_* methods)
|
|
232
265
|
Tina4::Middleware.run_after(Tina4::Middleware.global_middleware, request, final_response)
|
|
233
266
|
|
|
267
|
+
# Inject FreshToken header when body formToken was used for auth
|
|
268
|
+
if env["tina4.fresh_token"]
|
|
269
|
+
final_response.add_header("FreshToken", env["tina4.fresh_token"])
|
|
270
|
+
end
|
|
271
|
+
|
|
234
272
|
final_response.to_rack
|
|
235
273
|
end
|
|
236
274
|
|
|
@@ -740,5 +778,40 @@ module Tina4
|
|
|
740
778
|
end
|
|
741
779
|
|
|
742
780
|
|
|
781
|
+
# Read and rewind the Rack input body. Returns the raw body string.
|
|
782
|
+
def _read_rack_body(env)
|
|
783
|
+
input = env["rack.input"]
|
|
784
|
+
return "" unless input
|
|
785
|
+
input.rewind if input.respond_to?(:rewind)
|
|
786
|
+
body = input.read || ""
|
|
787
|
+
input.rewind if input.respond_to?(:rewind)
|
|
788
|
+
body
|
|
789
|
+
end
|
|
790
|
+
|
|
791
|
+
# Extract a formToken from the request body.
|
|
792
|
+
# Supports JSON body ({ "formToken": "..." }) and URL-encoded form data (formToken=...).
|
|
793
|
+
def _extract_form_token(body_str, env)
|
|
794
|
+
return nil if body_str.nil? || body_str.empty?
|
|
795
|
+
|
|
796
|
+
content_type = env["CONTENT_TYPE"] || env["HTTP_CONTENT_TYPE"] || ""
|
|
797
|
+
|
|
798
|
+
if content_type.include?("application/json")
|
|
799
|
+
begin
|
|
800
|
+
parsed = JSON.parse(body_str)
|
|
801
|
+
return parsed["formToken"] if parsed.is_a?(Hash) && parsed["formToken"]
|
|
802
|
+
rescue JSON::ParserError
|
|
803
|
+
# Not valid JSON — fall through
|
|
804
|
+
end
|
|
805
|
+
end
|
|
806
|
+
|
|
807
|
+
# URL-encoded form data (or fallback for any content type)
|
|
808
|
+
if body_str.include?("formToken=")
|
|
809
|
+
match = body_str.match(/(?:^|&)formToken=([^&]+)/)
|
|
810
|
+
return URI.decode_www_form_component(match[1]) if match
|
|
811
|
+
end
|
|
812
|
+
|
|
813
|
+
nil
|
|
814
|
+
end
|
|
815
|
+
|
|
743
816
|
end
|
|
744
817
|
end
|
data/lib/tina4/seeder.rb
CHANGED
|
@@ -524,6 +524,13 @@ module Tina4
|
|
|
524
524
|
results
|
|
525
525
|
end
|
|
526
526
|
|
|
527
|
+
# Run all seed files in the given folder.
|
|
528
|
+
#
|
|
529
|
+
# @param seed_folder [String] path to seed files (default: "seeds")
|
|
530
|
+
def self.seed(seed_folder: "seeds", clear: false)
|
|
531
|
+
seed_dir(seed_folder: seed_folder, clear: clear)
|
|
532
|
+
end
|
|
533
|
+
|
|
527
534
|
# Run all seed files in the given folder.
|
|
528
535
|
#
|
|
529
536
|
# @param seed_folder [String] path to seed files (default: "seeds")
|
data/lib/tina4/template.rb
CHANGED
|
@@ -230,16 +230,112 @@ module Tina4
|
|
|
230
230
|
parent_path = Regexp.last_match(1)
|
|
231
231
|
full_parent = resolve_template(parent_path)
|
|
232
232
|
if full_parent && File.exist?(full_parent)
|
|
233
|
-
|
|
234
|
-
content
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
content = @parent_template
|
|
233
|
+
parent_source = File.read(full_parent)
|
|
234
|
+
child_blocks = extract_blocks(content)
|
|
235
|
+
@blocks.merge!(child_blocks)
|
|
236
|
+
content = render_with_blocks(parent_source, @blocks)
|
|
238
237
|
end
|
|
239
238
|
end
|
|
240
239
|
content
|
|
241
240
|
end
|
|
242
241
|
|
|
242
|
+
# Extract {% block name %}...{% endblock %} pairs using depth counting
|
|
243
|
+
# so that nested blocks are handled correctly (non-greedy regex fails
|
|
244
|
+
# when blocks are nested inside other blocks).
|
|
245
|
+
def extract_blocks(source)
|
|
246
|
+
blocks = {}
|
|
247
|
+
block_open = /\{%[-\s]*block\s+(\w+)\s*-?%\}/
|
|
248
|
+
block_close = /\{%[-\s]*endblock\s*-?%\}/
|
|
249
|
+
|
|
250
|
+
pos = 0
|
|
251
|
+
while pos < source.length
|
|
252
|
+
m_open = block_open.match(source, pos)
|
|
253
|
+
break unless m_open
|
|
254
|
+
|
|
255
|
+
name = m_open[1]
|
|
256
|
+
content_start = m_open.end(0)
|
|
257
|
+
depth = 1
|
|
258
|
+
scan = content_start
|
|
259
|
+
|
|
260
|
+
while depth > 0 && scan < source.length
|
|
261
|
+
next_open = block_open.match(source, scan)
|
|
262
|
+
next_close = block_close.match(source, scan)
|
|
263
|
+
|
|
264
|
+
break unless next_close # malformed — no matching endblock
|
|
265
|
+
|
|
266
|
+
if next_open && next_open.begin(0) < next_close.begin(0)
|
|
267
|
+
depth += 1
|
|
268
|
+
scan = next_open.end(0)
|
|
269
|
+
else
|
|
270
|
+
depth -= 1
|
|
271
|
+
if depth == 0
|
|
272
|
+
blocks[name] = source[content_start...next_close.begin(0)]
|
|
273
|
+
pos = next_close.end(0)
|
|
274
|
+
break
|
|
275
|
+
end
|
|
276
|
+
scan = next_close.end(0)
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# If we didn't break out via depth==0, skip forward to avoid infinite loop
|
|
281
|
+
pos = content_start if depth > 0
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
blocks
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# Render a parent template replacing blocks with child overrides.
|
|
288
|
+
# Supports multi-level inheritance: if the parent itself extends a
|
|
289
|
+
# grandparent, blocks are merged (child overrides parent) and the
|
|
290
|
+
# chain is followed recursively.
|
|
291
|
+
def render_with_blocks(parent_source, child_blocks)
|
|
292
|
+
extends_re = /\A\s*\{%\s*extends\s+["'](.+?)["']\s*%\}/
|
|
293
|
+
|
|
294
|
+
# Multi-level: if the parent itself extends another template, recurse
|
|
295
|
+
if parent_source =~ extends_re
|
|
296
|
+
grandparent_name = Regexp.last_match(1)
|
|
297
|
+
full_grandparent = resolve_template(grandparent_name)
|
|
298
|
+
if full_grandparent && File.exist?(full_grandparent)
|
|
299
|
+
grandparent_source = File.read(full_grandparent)
|
|
300
|
+
|
|
301
|
+
# Extract block defaults defined in the parent template
|
|
302
|
+
parent_blocks = extract_blocks(parent_source)
|
|
303
|
+
|
|
304
|
+
# Child blocks override parent blocks at the same name
|
|
305
|
+
merged_blocks = parent_blocks.merge(child_blocks)
|
|
306
|
+
|
|
307
|
+
# Resolve nested blocks: if a block value contains {% block inner %}
|
|
308
|
+
# tags, replace them with merged_blocks values too
|
|
309
|
+
block_re = /\{%[-\s]*block\s+(\w+)\s*-?%\}(.*?)\{%[-\s]*endblock\s*-?%\}/m
|
|
310
|
+
changed = true
|
|
311
|
+
while changed
|
|
312
|
+
changed = false
|
|
313
|
+
merged_blocks.each do |bname, bsource|
|
|
314
|
+
resolved = bsource.gsub(block_re) do
|
|
315
|
+
inner_name = Regexp.last_match(1)
|
|
316
|
+
inner_default = Regexp.last_match(2)
|
|
317
|
+
merged_blocks[inner_name] || inner_default
|
|
318
|
+
end
|
|
319
|
+
if resolved != bsource
|
|
320
|
+
merged_blocks[bname] = resolved
|
|
321
|
+
changed = true
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# Recurse up the chain (handles 3+, 4+, ... levels)
|
|
327
|
+
return render_with_blocks(grandparent_source, merged_blocks)
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# Leaf parent (no extends) — resolve blocks and render
|
|
332
|
+
parent_source.gsub(/\{%[-\s]*block\s+(\w+)\s*-?%\}(.*?)\{%[-\s]*endblock\s*-?%\}/m) do
|
|
333
|
+
name = Regexp.last_match(1)
|
|
334
|
+
default_body = Regexp.last_match(2)
|
|
335
|
+
child_blocks[name] || default_body
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
|
|
243
339
|
def process_blocks(content)
|
|
244
340
|
content.gsub(/\{%\s*block\s+(\w+)\s*%\}(.*?)\{%\s*endblock\s*%\}/m) do
|
|
245
341
|
name = Regexp.last_match(1)
|
data/lib/tina4/version.rb
CHANGED
data/lib/tina4.rb
CHANGED
|
@@ -170,6 +170,9 @@ module Tina4
|
|
|
170
170
|
# Load translations
|
|
171
171
|
Tina4::Localization.load(root_dir)
|
|
172
172
|
|
|
173
|
+
# Auto-wire t() into template globals if locales were loaded
|
|
174
|
+
autowire_i18n_template_global
|
|
175
|
+
|
|
173
176
|
# Connect database if configured
|
|
174
177
|
setup_database
|
|
175
178
|
|
|
@@ -412,6 +415,17 @@ module Tina4
|
|
|
412
415
|
end
|
|
413
416
|
end
|
|
414
417
|
|
|
418
|
+
def autowire_i18n_template_global
|
|
419
|
+
# Only register if translations were actually loaded
|
|
420
|
+
return if Tina4::Localization.translations.empty?
|
|
421
|
+
|
|
422
|
+
# Don't overwrite a user-registered t() global
|
|
423
|
+
return if Tina4::Template.globals.key?("t")
|
|
424
|
+
|
|
425
|
+
Tina4::Template.add_global("t", ->(key, **opts) { Tina4::Localization.t(key, **opts) })
|
|
426
|
+
Tina4::Log.debug("Auto-wired i18n t() as template global")
|
|
427
|
+
end
|
|
428
|
+
|
|
415
429
|
def setup_database
|
|
416
430
|
db_url = ENV["DATABASE_URL"] || ENV["DB_URL"]
|
|
417
431
|
if db_url && !db_url.empty?
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: tina4ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.10.
|
|
4
|
+
version: 3.10.93
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tina4 Team
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-04-
|
|
11
|
+
date: 2026-04-11 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rack
|