ultra_settings 2.8.1 → 2.9.1
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/CHANGELOG.md +24 -2
- data/README.md +40 -1
- data/VERSION +1 -1
- data/app/AGENTS.md +7 -0
- data/app/_config_description.html.erb +22 -25
- data/app/_config_list.html.erb +2 -14
- data/app/_data_source.html.erb +43 -29
- data/app/application.css +1019 -340
- data/app/application.js +825 -90
- data/app/application_vars.css.erb +136 -91
- data/app/configuration.html.erb +59 -51
- data/app/index.html.erb +164 -20
- data/app/layout.css +81 -16
- data/app/layout.html.erb +67 -5
- data/app/layout_vars.css.erb +29 -5
- data/app/locales/ar.json +71 -0
- data/app/locales/cs.json +71 -0
- data/app/locales/da.json +71 -0
- data/app/locales/de.json +71 -0
- data/app/locales/el.json +71 -0
- data/app/locales/en.json +85 -0
- data/app/locales/es.json +71 -0
- data/app/locales/fa.json +71 -0
- data/app/locales/fr.json +71 -0
- data/app/locales/gd.json +71 -0
- data/app/locales/he.json +71 -0
- data/app/locales/hi.json +71 -0
- data/app/locales/it.json +71 -0
- data/app/locales/ja.json +71 -0
- data/app/locales/ko.json +71 -0
- data/app/locales/lt.json +71 -0
- data/app/locales/nb.json +71 -0
- data/app/locales/nl.json +71 -0
- data/app/locales/pl.json +71 -0
- data/app/locales/pt-br.json +71 -0
- data/app/locales/pt.json +71 -0
- data/app/locales/ru.json +71 -0
- data/app/locales/sv.json +71 -0
- data/app/locales/ta.json +71 -0
- data/app/locales/tr.json +71 -0
- data/app/locales/uk.json +71 -0
- data/app/locales/ur.json +71 -0
- data/app/locales/vi.json +71 -0
- data/app/locales/zh-cn.json +71 -0
- data/app/locales/zh-tw.json +71 -0
- data/lib/ultra_settings/application_view.rb +21 -3
- data/lib/ultra_settings/coerce.rb +0 -6
- data/lib/ultra_settings/config_helper.rb +4 -4
- data/lib/ultra_settings/configuration.rb +14 -4
- data/lib/ultra_settings/configuration_view.rb +114 -92
- data/lib/ultra_settings/mini_i18n.rb +110 -0
- data/lib/ultra_settings/rack_app.rb +51 -1
- data/lib/ultra_settings/version.rb +1 -1
- data/lib/ultra_settings/web_view.rb +33 -2
- data/lib/ultra_settings.rb +56 -22
- data/ultra_settings.gemspec +1 -0
- metadata +33 -3
- data/AGENTS.md +0 -191
- data/app/_select_menu.html.erb +0 -53
|
@@ -15,13 +15,13 @@ module UltraSettings
|
|
|
15
15
|
#
|
|
16
16
|
# @param config_class [Class] The configuration class to use.
|
|
17
17
|
# @return [void]
|
|
18
|
-
def configuration_class(config_class)
|
|
19
|
-
define_singleton_method
|
|
18
|
+
def configuration_class(config_class, config_alias: :config)
|
|
19
|
+
define_singleton_method config_alias do
|
|
20
20
|
config_class.instance
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
define_method
|
|
24
|
-
self.class.
|
|
23
|
+
define_method config_alias do
|
|
24
|
+
self.class.send(config_alias)
|
|
25
25
|
end
|
|
26
26
|
end
|
|
27
27
|
end
|
|
@@ -182,7 +182,7 @@ module UltraSettings
|
|
|
182
182
|
|
|
183
183
|
@configuration_file = default_configuration_file
|
|
184
184
|
end
|
|
185
|
-
return nil
|
|
185
|
+
return nil unless @configuration_file
|
|
186
186
|
|
|
187
187
|
path = @configuration_file
|
|
188
188
|
if path.relative? && yaml_config_path
|
|
@@ -560,7 +560,7 @@ module UltraSettings
|
|
|
560
560
|
|
|
561
561
|
sources = []
|
|
562
562
|
sources << :env if field.env_var
|
|
563
|
-
sources << :settings if field
|
|
563
|
+
sources << :settings if __runtime_setting_allowed?(field)
|
|
564
564
|
sources << :yaml if field.yaml_key && self.class.configuration_file
|
|
565
565
|
sources << :default unless field.default.nil?
|
|
566
566
|
sources
|
|
@@ -592,7 +592,8 @@ module UltraSettings
|
|
|
592
592
|
field = self.class.send(:defined_fields)[name]
|
|
593
593
|
return nil unless field
|
|
594
594
|
|
|
595
|
-
|
|
595
|
+
override_values = @ultra_settings_mutex.synchronize { @ultra_settings_override_values[Thread.current.object_id] }
|
|
596
|
+
use_override = override_values&.include?(name)
|
|
596
597
|
|
|
597
598
|
if field.static? && !use_override && @ultra_settings_memoized_values.include?(name)
|
|
598
599
|
return @ultra_settings_memoized_values[name]
|
|
@@ -600,7 +601,7 @@ module UltraSettings
|
|
|
600
601
|
|
|
601
602
|
value = nil
|
|
602
603
|
if use_override
|
|
603
|
-
value = field.coerce(
|
|
604
|
+
value = field.coerce(override_values[name])
|
|
604
605
|
else
|
|
605
606
|
env = ENV if field.env_var
|
|
606
607
|
settings = UltraSettings.__runtime_settings__ if field.runtime_setting
|
|
@@ -645,5 +646,14 @@ module UltraSettings
|
|
|
645
646
|
def __yaml_config__
|
|
646
647
|
@ultra_settings_yaml_config ||= self.class.load_yaml_config || {}
|
|
647
648
|
end
|
|
649
|
+
|
|
650
|
+
def __runtime_setting_allowed?(field)
|
|
651
|
+
return false unless UltraSettings.__runtime_settings__
|
|
652
|
+
return false if field.static?
|
|
653
|
+
return false unless field.runtime_setting
|
|
654
|
+
return false if field.secret? && !UltraSettings.runtime_settings_secure?
|
|
655
|
+
|
|
656
|
+
true
|
|
657
|
+
end
|
|
648
658
|
end
|
|
649
659
|
end
|
|
@@ -16,16 +16,18 @@ module UltraSettings
|
|
|
16
16
|
# Initialize the configuration view with a configuration instance.
|
|
17
17
|
#
|
|
18
18
|
# @param configuration [UltraSettings::Configuration] The configuration instance to display.
|
|
19
|
-
|
|
19
|
+
# @param locale [String] The locale code for translations.
|
|
20
|
+
def initialize(configuration, locale: UltraSettings::MiniI18n::DEFAULT_LOCALE)
|
|
20
21
|
@configuration = configuration
|
|
22
|
+
@locale = locale
|
|
21
23
|
end
|
|
22
24
|
|
|
23
25
|
# Render the HTML for the configuration view.
|
|
24
26
|
#
|
|
25
|
-
# @param table_class [String] CSS class for the table element (maintained for backwards compatibility).
|
|
27
|
+
# @param table_class [String] @deprecated CSS class for the table element (maintained for backwards compatibility).
|
|
26
28
|
# @return [String] The rendered HTML.
|
|
27
29
|
def render(table_class: "")
|
|
28
|
-
configuration = @configuration
|
|
30
|
+
configuration = @configuration # used by ERB template via binding
|
|
29
31
|
html = ViewHelper.erb_template("configuration.html.erb").result(binding)
|
|
30
32
|
html = html.html_safe if html.respond_to?(:html_safe)
|
|
31
33
|
html
|
|
@@ -40,6 +42,25 @@ module UltraSettings
|
|
|
40
42
|
|
|
41
43
|
private
|
|
42
44
|
|
|
45
|
+
# Map an UltraSettings field type to a SuperSettings value type.
|
|
46
|
+
#
|
|
47
|
+
# @param type [Symbol] The UltraSettings field type.
|
|
48
|
+
# @return [String] The corresponding SuperSettings value type.
|
|
49
|
+
def super_settings_value_type(type)
|
|
50
|
+
case type.to_sym
|
|
51
|
+
when :symbol then "string"
|
|
52
|
+
else type.to_s
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Look up a translation key for the current locale.
|
|
57
|
+
#
|
|
58
|
+
# @param key [String] dotted translation key
|
|
59
|
+
# @return [String]
|
|
60
|
+
def t(key)
|
|
61
|
+
UltraSettings::MiniI18n.t(key, locale: @locale)
|
|
62
|
+
end
|
|
63
|
+
|
|
43
64
|
def display_value(value)
|
|
44
65
|
case value
|
|
45
66
|
when Time
|
|
@@ -49,35 +70,9 @@ module UltraSettings
|
|
|
49
70
|
end
|
|
50
71
|
end
|
|
51
72
|
|
|
52
|
-
def show_defined_value(label, value, secret)
|
|
53
|
-
val = nil
|
|
54
|
-
icon = nil
|
|
55
|
-
css_class = nil
|
|
56
|
-
|
|
57
|
-
if value.nil?
|
|
58
|
-
val = "Not set"
|
|
59
|
-
icon = not_set_icon
|
|
60
|
-
css_class = "ultra-settings-icon-not-set"
|
|
61
|
-
elsif secret
|
|
62
|
-
val = secret_value(value)
|
|
63
|
-
icon = lock_icon
|
|
64
|
-
css_class = "ultra-settings-icon-secret"
|
|
65
|
-
else
|
|
66
|
-
val = display_value(value)
|
|
67
|
-
icon = eye_icon
|
|
68
|
-
css_class = "ultra-settings-icon-info"
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
<<~HTML
|
|
72
|
-
<dfn class="#{css_class}" title="#{html_escape(val)}" onclick="#{html_escape(open_dialog_script)}" data-label="#{html_escape(label)}">
|
|
73
|
-
#{icon}
|
|
74
|
-
</dfn>
|
|
75
|
-
HTML
|
|
76
|
-
end
|
|
77
|
-
|
|
78
73
|
def secret_value(value)
|
|
79
74
|
if value.nil?
|
|
80
|
-
"nil"
|
|
75
|
+
t("field.nil")
|
|
81
76
|
else
|
|
82
77
|
"••••••••••••••••"
|
|
83
78
|
end
|
|
@@ -92,22 +87,79 @@ module UltraSettings
|
|
|
92
87
|
path.relative_path_from(root_path)
|
|
93
88
|
end
|
|
94
89
|
|
|
95
|
-
def
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
90
|
+
def source_chip_label(source)
|
|
91
|
+
case source
|
|
92
|
+
when :env then t("source.env")
|
|
93
|
+
when :settings then t("source.setting")
|
|
94
|
+
when :yaml then t("source.yaml")
|
|
95
|
+
when :default then t("source.default")
|
|
96
|
+
else source.to_s.upcase
|
|
97
|
+
end
|
|
102
98
|
end
|
|
103
99
|
|
|
104
|
-
def
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
100
|
+
def source_chip_class(source)
|
|
101
|
+
case source
|
|
102
|
+
when :env then "ultra-settings-chip-env"
|
|
103
|
+
when :settings then "ultra-settings-chip-setting"
|
|
104
|
+
when :yaml then "ultra-settings-chip-yaml"
|
|
105
|
+
when :default then "ultra-settings-chip-default"
|
|
106
|
+
else "ultra-settings-chip-default"
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def source_key_name(field, source)
|
|
111
|
+
case source
|
|
112
|
+
when :env then field.env_var
|
|
113
|
+
when :settings then field.runtime_setting
|
|
114
|
+
when :yaml then field.yaml_key
|
|
115
|
+
when :default then nil
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def open_panel_script
|
|
120
|
+
<<~JAVASCRIPT.gsub(/\s+/, " ").tr('"', "'")
|
|
121
|
+
var el = this;
|
|
122
|
+
var panel = document.getElementById('ultra-settings-detail-panel');
|
|
123
|
+
if (panel) {
|
|
124
|
+
var name = el.dataset.name || '';
|
|
125
|
+
var value = el.dataset.value || '';
|
|
126
|
+
var type = el.dataset.type || '';
|
|
127
|
+
var isSecret = el.dataset.secret || 'false';
|
|
128
|
+
document.getElementById('ultra-settings-dp-title').textContent = name;
|
|
129
|
+
document.getElementById('ultra-settings-dp-value').textContent = isSecret === 'true' ? window.__ultraSettingsI18n['detail.secret_value'] : value;
|
|
130
|
+
document.getElementById('ultra-settings-dp-meta').innerHTML = window.__ultraSettingsI18n['detail.type_label'] + ' <span>' + type.toUpperCase() + '</span>' + (isSecret === 'true' ? ' \u00B7 <span style=color:var(--badge-secret-text)>' + window.__ultraSettingsI18n['detail.secret_badge'] + '</span>' : '');
|
|
131
|
+
document.getElementById('ultra-settings-panel-bg').classList.add('open');
|
|
132
|
+
panel.classList.add('open');
|
|
133
|
+
} else {
|
|
134
|
+
var block = el.closest('.ultra-settings-block');
|
|
135
|
+
if (block) {
|
|
136
|
+
var dialog = block.querySelector('.ultra-settings-dialog');
|
|
137
|
+
if (dialog) {
|
|
138
|
+
var title = dialog.querySelector('.ultra-settings-dialog-title');
|
|
139
|
+
var val = dialog.querySelector('.ultra-settings-dialog-value');
|
|
140
|
+
if (title) title.textContent = el.dataset.name || '';
|
|
141
|
+
if (val) val.textContent = el.dataset.value || '';
|
|
142
|
+
dialog.showModal();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
JAVASCRIPT
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def source_priority
|
|
150
|
+
[:env, :settings, :yaml, :default]
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def source_overridden_by(current_source, active_source)
|
|
154
|
+
return nil if current_source == active_source
|
|
155
|
+
|
|
156
|
+
current_index = source_priority.index(current_source)
|
|
157
|
+
active_index = source_priority.index(active_source)
|
|
158
|
+
|
|
159
|
+
return nil if current_index.nil? || active_index.nil?
|
|
160
|
+
return nil if current_index < active_index
|
|
161
|
+
|
|
162
|
+
active_source
|
|
111
163
|
end
|
|
112
164
|
|
|
113
165
|
def lock_icon(size = 16)
|
|
@@ -118,77 +170,47 @@ module UltraSettings
|
|
|
118
170
|
HTML
|
|
119
171
|
end
|
|
120
172
|
|
|
121
|
-
def
|
|
173
|
+
def pin_icon(size = 16)
|
|
122
174
|
<<~HTML
|
|
123
175
|
<svg width="#{size}" height="#{size}" fill="currentColor" viewBox="0 0 16 16">
|
|
124
|
-
<path d="
|
|
125
|
-
<path fill-rule="evenodd" d="M1 13.5A1.5 1.5 0 0 0 2.5 15h11a1.5 1.5 0 0 0 1.5-1.5v-6a.5.5 0 0 0-1 0v6a.5.5 0 0 1-.5.5h-11a.5.5 0 0 1-.5-.5v-11a.5.5 0 0 1 .5-.5H9a.5.5 0 0 0 0-1H2.5A1.5 1.5 0 0 0 1 2.5z"/>
|
|
176
|
+
<path d="M4.146.146A.5.5 0 0 1 4.5 0h7a.5.5 0 0 1 .5.5c0 .68-.342 1.174-.646 1.479-.126.125-.25.224-.354.298v4.431l.078.048c.203.127.476.314.751.555C12.36 7.775 13 8.527 13 9.5a.5.5 0 0 1-.5.5h-4v4.5a.5.5 0 0 1-1 0V10h-4A.5.5 0 0 1 3 9.5c0-.973.64-1.725 1.17-2.189A6 6 0 0 1 5 6.708V2.277a3 3 0 0 1-.354-.298C4.342 1.674 4 1.179 4 .5a.5.5 0 0 1 .146-.354"/>
|
|
126
177
|
</svg>
|
|
127
178
|
HTML
|
|
128
179
|
end
|
|
129
180
|
|
|
130
|
-
def
|
|
181
|
+
def file_icon(size = 13)
|
|
131
182
|
<<~HTML
|
|
132
|
-
<svg width="#{size}" height="#{size}"
|
|
133
|
-
<path d="
|
|
134
|
-
<
|
|
183
|
+
<svg width="#{size}" height="#{size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
184
|
+
<path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/>
|
|
185
|
+
<polyline points="13 2 13 9 20 9"/>
|
|
135
186
|
</svg>
|
|
136
187
|
HTML
|
|
137
188
|
end
|
|
138
189
|
|
|
139
|
-
def
|
|
190
|
+
def edit_icon(size = 16)
|
|
140
191
|
<<~HTML
|
|
141
192
|
<svg width="#{size}" height="#{size}" fill="currentColor" viewBox="0 0 16 16">
|
|
142
|
-
<path d="
|
|
143
|
-
<path d="
|
|
193
|
+
<path d="M15.502 1.94a.5.5 0 0 1 0 .706L14.459 3.69l-2-2L13.502.646a.5.5 0 0 1 .707 0l1.293 1.293zm-1.75 2.456-2-2L4.939 9.21a.5.5 0 0 0-.121.196l-.805 2.414a.25.25 0 0 0 .316.316l2.414-.805a.5.5 0 0 0 .196-.12l6.813-6.814z"/>
|
|
194
|
+
<path fill-rule="evenodd" d="M1 13.5A1.5 1.5 0 0 0 2.5 15h11a1.5 1.5 0 0 0 1.5-1.5v-6a.5.5 0 0 0-1 0v6a.5.5 0 0 1-.5.5h-11a.5.5 0 0 1-.5-.5v-11a.5.5 0 0 1 .5-.5H9a.5.5 0 0 0 0-1H2.5A1.5 1.5 0 0 0 1 2.5z"/>
|
|
144
195
|
</svg>
|
|
145
196
|
HTML
|
|
146
197
|
end
|
|
147
198
|
|
|
148
|
-
def
|
|
149
|
-
<<~JAVASCRIPT.gsub(/\s+/, " ").tr('"', "'")
|
|
150
|
-
this.closest('.ultra-settings-configuration').querySelector('.ultra-settings-dialog-title').textContent = this.dataset.label;
|
|
151
|
-
this.closest('.ultra-settings-configuration').querySelector('.ultra-settings-dialog-value').textContent = this.title;
|
|
152
|
-
this.closest('.ultra-settings-configuration').querySelector('.ultra-settings-dialog').showModal();
|
|
153
|
-
this.closest('.ultra-settings-configuration').querySelector('.ultra-settings-dialog-close').blur();
|
|
154
|
-
JAVASCRIPT
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
def source_priority
|
|
158
|
-
[:env, :settings, :yaml, :default]
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
def source_overridden_by(current_source, active_source)
|
|
162
|
-
return nil if current_source == active_source
|
|
163
|
-
|
|
164
|
-
current_index = source_priority.index(current_source)
|
|
165
|
-
active_index = source_priority.index(active_source)
|
|
166
|
-
|
|
167
|
-
return nil if current_index.nil? || active_index.nil?
|
|
168
|
-
return nil if current_index < active_index
|
|
169
|
-
|
|
170
|
-
active_source
|
|
171
|
-
end
|
|
172
|
-
|
|
173
|
-
def override_indicator(overridden_by_source)
|
|
174
|
-
source_names = {
|
|
175
|
-
env: "environment variable",
|
|
176
|
-
settings: "runtime setting",
|
|
177
|
-
yaml: "configuration file",
|
|
178
|
-
default: "default value"
|
|
179
|
-
}
|
|
199
|
+
def close_icon(size = 16)
|
|
180
200
|
<<~HTML
|
|
181
|
-
<
|
|
182
|
-
|
|
183
|
-
<
|
|
184
|
-
</
|
|
201
|
+
<svg width="#{size}" height="#{size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
|
202
|
+
<line x1="18" y1="6" x2="6" y2="18"/>
|
|
203
|
+
<line x1="6" y1="6" x2="18" y2="18"/>
|
|
204
|
+
</svg>
|
|
185
205
|
HTML
|
|
186
206
|
end
|
|
187
207
|
|
|
188
208
|
def warning_icon(size = 16)
|
|
189
209
|
<<~HTML
|
|
190
|
-
<svg width="#{size}" height="#{size}"
|
|
191
|
-
<path d="
|
|
210
|
+
<svg width="#{size}" height="#{size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" xmlns="http://www.w3.org/2000/svg">
|
|
211
|
+
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" stroke-width="2" stroke-linejoin="round"/>
|
|
212
|
+
<line x1="12" y1="10" x2="12" y2="14" stroke-width="2.5" stroke-linecap="round"/>
|
|
213
|
+
<circle cx="12" cy="17" r="1" fill="currentColor"/>
|
|
192
214
|
</svg>
|
|
193
215
|
HTML
|
|
194
216
|
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module UltraSettings
|
|
6
|
+
# Internationalization support for the web UI. Translations are stored as
|
|
7
|
+
# JSON files in +app/locales/+ with one file per locale (e.g. +en.json+).
|
|
8
|
+
#
|
|
9
|
+
# Ruby templates use `#t` to look up a dotted key. JavaScript receives the
|
|
10
|
+
# full translation hash inlined via a +<script>+ tag so that the same JSON
|
|
11
|
+
# file drives both server-side and client-side strings.
|
|
12
|
+
module MiniI18n
|
|
13
|
+
DEFAULT_LOCALE = "en"
|
|
14
|
+
|
|
15
|
+
@cache = {}
|
|
16
|
+
@mutex = Mutex.new
|
|
17
|
+
|
|
18
|
+
class << self
|
|
19
|
+
# Return the list of available locale codes derived from the JSON files
|
|
20
|
+
# present in +app/locales/+.
|
|
21
|
+
#
|
|
22
|
+
# @return [Array<String>] locale codes, e.g. +["en"]+
|
|
23
|
+
def available_locales
|
|
24
|
+
load_all_locales.keys.sort
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Look up a translation by dotted key for the given locale.
|
|
28
|
+
# Falls back to the default locale when the key is missing, and
|
|
29
|
+
# ultimately returns the key itself if no translation is found.
|
|
30
|
+
#
|
|
31
|
+
# @param key [String] dotted translation key, e.g. +"page.title"+
|
|
32
|
+
# @param locale [String] the locale code (default: +DEFAULT_LOCALE+)
|
|
33
|
+
# @return [String] the translated string
|
|
34
|
+
def t(key, locale: DEFAULT_LOCALE)
|
|
35
|
+
translations = translations_for(locale)
|
|
36
|
+
translations[key] || translations_for(DEFAULT_LOCALE)[key] || key
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Return the full translation hash for a locale. Used to inline
|
|
40
|
+
# translations into the HTML page for JavaScript consumption.
|
|
41
|
+
#
|
|
42
|
+
# @param locale [String] the locale code
|
|
43
|
+
# @return [Hash<String, String>] all key/value translations
|
|
44
|
+
def translations_for(locale)
|
|
45
|
+
load_all_locales[normalize_locale(locale)] || load_all_locales[DEFAULT_LOCALE] || {}
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Return the text direction for the given locale. Reads the +"dir"+ key
|
|
49
|
+
# from the locale's translations hash, falling back to +"ltr"+ when not set.
|
|
50
|
+
#
|
|
51
|
+
# @param locale [String] the locale code
|
|
52
|
+
# @return [String] +"ltr"+ or +"rtl"+
|
|
53
|
+
def text_direction(locale = DEFAULT_LOCALE)
|
|
54
|
+
dir = translations_for(locale)["dir"]
|
|
55
|
+
(dir == "rtl") ? "rtl" : "ltr"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Clear the translation cache. Called automatically in development mode.
|
|
59
|
+
#
|
|
60
|
+
# @return [void]
|
|
61
|
+
def clear_cache!
|
|
62
|
+
@mutex.synchronize { @cache = {} }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
# Normalize a locale string to just the language subtag if the full
|
|
68
|
+
# tag is not available (e.g. "en-US" → "en").
|
|
69
|
+
def normalize_locale(locale)
|
|
70
|
+
locale = locale.to_s.strip.tr("_", "-").downcase
|
|
71
|
+
return locale if @cache.key?(locale)
|
|
72
|
+
|
|
73
|
+
# Try the language subtag (e.g. "en-us" → "en")
|
|
74
|
+
lang = locale.split("-").first
|
|
75
|
+
lang if @cache.key?(lang)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Load every JSON file from the locales directory, keyed by filename
|
|
79
|
+
# stem (e.g. "en").
|
|
80
|
+
def load_all_locales
|
|
81
|
+
if development_mode?
|
|
82
|
+
@mutex.synchronize { @cache = {} }
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
return @cache unless @cache.empty?
|
|
86
|
+
|
|
87
|
+
@mutex.synchronize do
|
|
88
|
+
return @cache unless @cache.empty?
|
|
89
|
+
|
|
90
|
+
Dir.glob(File.join(locales_dir, "*.json")).each do |path|
|
|
91
|
+
code = File.basename(path, ".json").downcase
|
|
92
|
+
@cache[code] = JSON.parse(File.read(path))
|
|
93
|
+
rescue JSON::ParserError
|
|
94
|
+
# Skip malformed locale files
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
@cache
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def locales_dir
|
|
102
|
+
File.expand_path(File.join("..", "..", "app", "locales"), __dir__)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def development_mode?
|
|
106
|
+
ENV.fetch("RACK_ENV", "development") == "development"
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -18,7 +18,9 @@ module UltraSettings
|
|
|
18
18
|
# @param env [Hash] The Rack environment.
|
|
19
19
|
# @return [Array] A Rack response array [status, headers, body].
|
|
20
20
|
def call(env)
|
|
21
|
-
|
|
21
|
+
request = Rack::Request.new(env)
|
|
22
|
+
locale = resolve_locale(request)
|
|
23
|
+
[200, {"content-type" => "text/html; charset=utf8"}, [webview.render_settings(request, locale: locale)]]
|
|
22
24
|
end
|
|
23
25
|
|
|
24
26
|
private
|
|
@@ -29,5 +31,53 @@ module UltraSettings
|
|
|
29
31
|
end
|
|
30
32
|
@webview ||= WebView.new(color_scheme: @color_scheme)
|
|
31
33
|
end
|
|
34
|
+
|
|
35
|
+
# Determine the locale for a request. Precedence:
|
|
36
|
+
# 1. ?lang= query parameter
|
|
37
|
+
# 2. ultra_settings_locale cookie (set by the language picker)
|
|
38
|
+
# 3. Accept-Language header
|
|
39
|
+
# 4. Default locale
|
|
40
|
+
def resolve_locale(request)
|
|
41
|
+
available = UltraSettings::MiniI18n.available_locales
|
|
42
|
+
|
|
43
|
+
# 1. Explicit query parameter
|
|
44
|
+
lang = request.params["lang"] if request.respond_to?(:params)
|
|
45
|
+
return lang if lang && available.include?(lang)
|
|
46
|
+
|
|
47
|
+
# 2. Cookie
|
|
48
|
+
cookie = request.cookies["ultra_settings_locale"] if request.respond_to?(:cookies)
|
|
49
|
+
return cookie if cookie && available.include?(cookie)
|
|
50
|
+
|
|
51
|
+
# 3. Accept-Language header
|
|
52
|
+
accept = request.env["HTTP_ACCEPT_LANGUAGE"] if request.respond_to?(:env)
|
|
53
|
+
locale_from_accept_language(accept.to_s, available) || UltraSettings::MiniI18n::DEFAULT_LOCALE
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Parse the Accept-Language header and return the best matching locale.
|
|
57
|
+
def locale_from_accept_language(header, available)
|
|
58
|
+
return nil if header.nil? || header.empty?
|
|
59
|
+
|
|
60
|
+
# Parse tags with optional quality values, e.g. "en-US,en;q=0.9,fr;q=0.8"
|
|
61
|
+
tags = header.split(",").map { |entry|
|
|
62
|
+
parts = entry.strip.split(";")
|
|
63
|
+
tag = parts[0].to_s.strip.downcase.tr("_", "-")
|
|
64
|
+
q = 1.0
|
|
65
|
+
parts[1..-1].each do |p|
|
|
66
|
+
if p.strip.start_with?("q=")
|
|
67
|
+
q = p.strip.sub("q=", "").to_f
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
[tag, q]
|
|
71
|
+
}.sort_by { |_, q| -q }
|
|
72
|
+
|
|
73
|
+
tags.each do |tag, _|
|
|
74
|
+
return tag if available.include?(tag)
|
|
75
|
+
# Try language subtag
|
|
76
|
+
lang = tag.split("-").first
|
|
77
|
+
return lang if available.include?(lang)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
nil
|
|
81
|
+
end
|
|
32
82
|
end
|
|
33
83
|
end
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
module UltraSettings
|
|
4
4
|
# Helper class for rendering the settings information in an HTML page.
|
|
5
5
|
class WebView
|
|
6
|
+
include RenderHelper
|
|
7
|
+
|
|
6
8
|
attr_reader :layout_css
|
|
7
9
|
|
|
8
10
|
# Initialize a new WebView with the specified color scheme.
|
|
@@ -17,8 +19,13 @@ module UltraSettings
|
|
|
17
19
|
|
|
18
20
|
# Render the complete settings page HTML.
|
|
19
21
|
#
|
|
22
|
+
# @param request [Rack::Request, nil] The current Rack request for access control.
|
|
23
|
+
# @param locale [String] The locale code for translations.
|
|
20
24
|
# @return [String] The rendered HTML page.
|
|
21
|
-
def render_settings
|
|
25
|
+
def render_settings(request = nil, locale: UltraSettings::MiniI18n::DEFAULT_LOCALE)
|
|
26
|
+
@request = request
|
|
27
|
+
@locale = locale
|
|
28
|
+
refresh_super_settings!
|
|
22
29
|
@layout_template.result(binding)
|
|
23
30
|
end
|
|
24
31
|
|
|
@@ -26,7 +33,25 @@ module UltraSettings
|
|
|
26
33
|
#
|
|
27
34
|
# @return [String] The HTML content for the settings.
|
|
28
35
|
def content
|
|
29
|
-
UltraSettings::ApplicationView.new(
|
|
36
|
+
UltraSettings::ApplicationView.new(
|
|
37
|
+
color_scheme: @color_scheme,
|
|
38
|
+
locale: @locale || UltraSettings::MiniI18n::DEFAULT_LOCALE
|
|
39
|
+
).render
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Look up a translation key for the current locale.
|
|
43
|
+
#
|
|
44
|
+
# @param key [String] dotted translation key
|
|
45
|
+
# @return [String]
|
|
46
|
+
def t(key)
|
|
47
|
+
UltraSettings::MiniI18n.t(key, locale: @locale || UltraSettings::MiniI18n::DEFAULT_LOCALE)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Return the text direction (+"ltr"+ or +"rtl"+) for the current locale.
|
|
51
|
+
#
|
|
52
|
+
# @return [String]
|
|
53
|
+
def text_direction
|
|
54
|
+
UltraSettings::MiniI18n.text_direction(@locale || UltraSettings::MiniI18n::DEFAULT_LOCALE)
|
|
30
55
|
end
|
|
31
56
|
|
|
32
57
|
private
|
|
@@ -36,5 +61,11 @@ module UltraSettings
|
|
|
36
61
|
css = ViewHelper.read_app_file("layout.css")
|
|
37
62
|
"#{vars}\n#{css}"
|
|
38
63
|
end
|
|
64
|
+
|
|
65
|
+
def refresh_super_settings!
|
|
66
|
+
return unless defined?(SuperSettings) && UltraSettings.__runtime_settings__ == SuperSettings
|
|
67
|
+
|
|
68
|
+
SuperSettings.refresh_settings
|
|
69
|
+
end
|
|
39
70
|
end
|
|
40
71
|
end
|