ultra_settings 2.8.0 → 2.9.0
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 -1
- data/MIT-LICENSE.txt +1 -1
- data/README.md +108 -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 +53 -0
- data/app/application.css +1078 -259
- data/app/application.js +818 -91
- data/app/application_vars.css.erb +136 -81
- data/app/configuration.html.erb +60 -107
- 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/audit_data_sources.rb +98 -0
- data/lib/ultra_settings/coerce.rb +0 -6
- data/lib/ultra_settings/config_helper.rb +4 -4
- data/lib/ultra_settings/configuration.rb +28 -7
- data/lib/ultra_settings/configuration_view.rb +117 -56
- data/lib/ultra_settings/mini_i18n.rb +110 -0
- data/lib/ultra_settings/rack_app.rb +51 -1
- data/lib/ultra_settings/railtie.rb +8 -0
- data/lib/ultra_settings/tasks/audit_data_sources.rake +76 -0
- data/lib/ultra_settings/tasks/utils.rb +23 -0
- 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 +3 -0
- metadata +38 -3
- data/app/_select_menu.html.erb +0 -53
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module UltraSettings
|
|
4
|
+
class AuditDataSources
|
|
5
|
+
class << self
|
|
6
|
+
# Find environment variables that are set but have the same value as their default.
|
|
7
|
+
# These environment variables could potentially be removed since they're not changing behavior.
|
|
8
|
+
#
|
|
9
|
+
# @return [Array<Array<(String, Object)>>] An array of tuples containing environment variable names and their default values
|
|
10
|
+
def unnecessary_env_vars
|
|
11
|
+
env_vars_at_default = []
|
|
12
|
+
each_configuration do |config|
|
|
13
|
+
each_field_using_source(config, :env) do |field|
|
|
14
|
+
value = config[field.name]
|
|
15
|
+
default_value = default_config_value(config, field)
|
|
16
|
+
env_vars_at_default << [field.env_var, default_value] if default_value == value
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
env_vars_at_default
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Find runtime settings that are set but have the same value as their default.
|
|
23
|
+
# These runtime settings could potentially be removed since they're not changing behavior.
|
|
24
|
+
#
|
|
25
|
+
# @return [Array<Array<(String, Object)>>] An array of tuples containing runtime setting names and their default values
|
|
26
|
+
def unnecessary_runtime_settings
|
|
27
|
+
unnecessary_runtime_settings = []
|
|
28
|
+
each_configuration do |config|
|
|
29
|
+
each_field_using_source(config, :settings) do |field|
|
|
30
|
+
value = config[field.name]
|
|
31
|
+
default_value = default_config_value(config, field)
|
|
32
|
+
unnecessary_runtime_settings << [field.runtime_setting, default_value] if default_value == value
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
unnecessary_runtime_settings
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Find environment variables that could be moved to runtime settings.
|
|
39
|
+
# These are non-default environment variable values where a runtime setting is also available.
|
|
40
|
+
#
|
|
41
|
+
# @return [Array<Array<(String, String, Object)>>] An array of tuples containing environment variable name, runtime setting name, and current value
|
|
42
|
+
def env_vars_can_be_runtime_setting
|
|
43
|
+
env_vars_can_be_runtime = []
|
|
44
|
+
each_configuration do |config|
|
|
45
|
+
each_field_using_source(config, :env) do |field|
|
|
46
|
+
value = config[field.name]
|
|
47
|
+
default_value = default_config_value(config, field)
|
|
48
|
+
next unless field.runtime_setting && value != default_value
|
|
49
|
+
|
|
50
|
+
env_vars_can_be_runtime << [field.env_var, field.runtime_setting, value]
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
env_vars_can_be_runtime
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Find environment variables being used that don't have default values defined.
|
|
57
|
+
# These configurations require an environment variable to be set.
|
|
58
|
+
#
|
|
59
|
+
# @return [Array<Array<(String, Symbol, String, Object)>>] An array of tuples containing class name, field name, environment variable name, and current value
|
|
60
|
+
def env_vars_without_default
|
|
61
|
+
no_default_env_var_fields = []
|
|
62
|
+
each_configuration do |config|
|
|
63
|
+
each_field_using_source(config, :env) do |field|
|
|
64
|
+
value = default_config_value(config, field)
|
|
65
|
+
if value.nil?
|
|
66
|
+
no_default_env_var_fields << [config.class.name, field.name, field.env_var, config[field.name]]
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
no_default_env_var_fields
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def each_configuration(&_block)
|
|
76
|
+
UltraSettings::Configuration.descendant_configurations.each do |config_class|
|
|
77
|
+
config = config_class.instance
|
|
78
|
+
yield config
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def each_field_using_source(config, source, &_block)
|
|
83
|
+
config.class.fields.each do |field|
|
|
84
|
+
next if field.secret?
|
|
85
|
+
next unless config.__source__(field.name) == source
|
|
86
|
+
|
|
87
|
+
yield field
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def default_config_value(config, field)
|
|
92
|
+
yaml_value = config.__value_from_source__(field.name, :yaml)
|
|
93
|
+
default_value = config.__value_from_source__(field.name, :default)
|
|
94
|
+
yaml_value || default_value
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -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
|
|
@@ -10,6 +10,7 @@ module UltraSettings
|
|
|
10
10
|
@env_var_prefix = nil
|
|
11
11
|
@runtime_setting_prefix = nil
|
|
12
12
|
@description = nil
|
|
13
|
+
@descendants = []
|
|
13
14
|
|
|
14
15
|
class << self
|
|
15
16
|
# Set a description for the configuration. This is optional. It will be displayed
|
|
@@ -84,7 +85,8 @@ module UltraSettings
|
|
|
84
85
|
secret: secret
|
|
85
86
|
)
|
|
86
87
|
|
|
87
|
-
|
|
88
|
+
caller_location = caller_locations(1, 1).first
|
|
89
|
+
class_eval <<~RUBY, caller_location.path, caller_location.lineno # rubocop:disable Security/Eval, Style/EvalWithLocation
|
|
88
90
|
def #{name}
|
|
89
91
|
__get_value__(#{name.inspect})
|
|
90
92
|
end
|
|
@@ -110,7 +112,7 @@ module UltraSettings
|
|
|
110
112
|
name = name.to_s
|
|
111
113
|
return true if defined_fields.include?(name)
|
|
112
114
|
|
|
113
|
-
if superclass
|
|
115
|
+
if superclass < Configuration
|
|
114
116
|
superclass.include_field?(name)
|
|
115
117
|
else
|
|
116
118
|
false
|
|
@@ -180,7 +182,7 @@ module UltraSettings
|
|
|
180
182
|
|
|
181
183
|
@configuration_file = default_configuration_file
|
|
182
184
|
end
|
|
183
|
-
return nil
|
|
185
|
+
return nil unless @configuration_file
|
|
184
186
|
|
|
185
187
|
path = @configuration_file
|
|
186
188
|
if path.relative? && yaml_config_path
|
|
@@ -369,12 +371,30 @@ module UltraSettings
|
|
|
369
371
|
YamlConfig.new(configuration_file, yaml_config_env).to_h
|
|
370
372
|
end
|
|
371
373
|
|
|
374
|
+
# Get all descendant configuration classes (subclasses and their subclasses, recursively).
|
|
375
|
+
#
|
|
376
|
+
# @return [Array<Class>] All classes that inherit from this class.
|
|
377
|
+
def descendant_configurations
|
|
378
|
+
@descendants ||= []
|
|
379
|
+
@descendants.flat_map { |subclass| [subclass] + subclass.descendant_configurations }
|
|
380
|
+
end
|
|
381
|
+
|
|
372
382
|
private
|
|
373
383
|
|
|
384
|
+
# Hook called when this class is inherited. Tracks all descendant classes.
|
|
385
|
+
#
|
|
386
|
+
# @param subclass [Class] The subclass that is inheriting from this class.
|
|
387
|
+
# @return [void]
|
|
388
|
+
def inherited(subclass)
|
|
389
|
+
super
|
|
390
|
+
@descendants ||= []
|
|
391
|
+
@descendants << subclass
|
|
392
|
+
end
|
|
393
|
+
|
|
374
394
|
def defined_fields
|
|
375
395
|
unless defined?(@defined_fields)
|
|
376
396
|
fields = {}
|
|
377
|
-
if superclass
|
|
397
|
+
if superclass < Configuration
|
|
378
398
|
fields = superclass.send(:defined_fields).dup
|
|
379
399
|
end
|
|
380
400
|
@defined_fields = fields
|
|
@@ -540,7 +560,7 @@ module UltraSettings
|
|
|
540
560
|
|
|
541
561
|
sources = []
|
|
542
562
|
sources << :env if field.env_var
|
|
543
|
-
sources << :settings if field.runtime_setting && UltraSettings.__runtime_settings__
|
|
563
|
+
sources << :settings if !field.static? && field.runtime_setting && UltraSettings.__runtime_settings__
|
|
544
564
|
sources << :yaml if field.yaml_key && self.class.configuration_file
|
|
545
565
|
sources << :default unless field.default.nil?
|
|
546
566
|
sources
|
|
@@ -572,7 +592,8 @@ module UltraSettings
|
|
|
572
592
|
field = self.class.send(:defined_fields)[name]
|
|
573
593
|
return nil unless field
|
|
574
594
|
|
|
575
|
-
|
|
595
|
+
override_values = @ultra_settings_mutex.synchronize { @ultra_settings_override_values[Thread.current.object_id] }
|
|
596
|
+
use_override = override_values&.include?(name)
|
|
576
597
|
|
|
577
598
|
if field.static? && !use_override && @ultra_settings_memoized_values.include?(name)
|
|
578
599
|
return @ultra_settings_memoized_values[name]
|
|
@@ -580,7 +601,7 @@ module UltraSettings
|
|
|
580
601
|
|
|
581
602
|
value = nil
|
|
582
603
|
if use_override
|
|
583
|
-
value = field.coerce(
|
|
604
|
+
value = field.coerce(override_values[name])
|
|
584
605
|
else
|
|
585
606
|
env = ENV if field.env_var
|
|
586
607
|
settings = UltraSettings.__runtime_settings__ if field.runtime_setting
|
|
@@ -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,28 +87,102 @@ module UltraSettings
|
|
|
92
87
|
path.relative_path_from(root_path)
|
|
93
88
|
end
|
|
94
89
|
|
|
95
|
-
def
|
|
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
|
|
98
|
+
end
|
|
99
|
+
|
|
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
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def lock_icon(size = 16)
|
|
96
166
|
<<~HTML
|
|
97
167
|
<svg width="#{size}" height="#{size}" fill="currentColor" viewBox="0 0 16 16">
|
|
98
|
-
<path d="M8
|
|
99
|
-
<path d="m8.93 6.588-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246-.275 0-.375-.193-.304-.533zM9 4.5a1 1 0 1 1-2 0 1 1 0 0 1 2 0"/>
|
|
168
|
+
<path fill-rule="evenodd" d="M8 0a4 4 0 0 1 4 4v2.05a2.5 2.5 0 0 1 2 2.45v5a2.5 2.5 0 0 1-2.5 2.5h-7A2.5 2.5 0 0 1 2 13.5v-5a2.5 2.5 0 0 1 2-2.45V4a4 4 0 0 1 4-4m0 1a3 3 0 0 0-3 3v2h6V4a3 3 0 0 0-3-3"/>
|
|
100
169
|
</svg>
|
|
101
170
|
HTML
|
|
102
171
|
end
|
|
103
172
|
|
|
104
|
-
def
|
|
173
|
+
def pin_icon(size = 16)
|
|
105
174
|
<<~HTML
|
|
106
175
|
<svg width="#{size}" height="#{size}" fill="currentColor" viewBox="0 0 16 16">
|
|
107
|
-
<path d="
|
|
108
|
-
<path d="M11.354 4.646a.5.5 0 0 0-.708 0l-6 6a.5.5 0 0 0 .708.708l6-6a.5.5 0 0 0 0-.708"/>
|
|
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"/>
|
|
109
177
|
</svg>
|
|
110
178
|
HTML
|
|
111
179
|
end
|
|
112
180
|
|
|
113
|
-
def
|
|
181
|
+
def file_icon(size = 13)
|
|
114
182
|
<<~HTML
|
|
115
|
-
<svg width="#{size}" height="#{size}"
|
|
116
|
-
<path
|
|
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"/>
|
|
117
186
|
</svg>
|
|
118
187
|
HTML
|
|
119
188
|
end
|
|
@@ -127,31 +196,23 @@ module UltraSettings
|
|
|
127
196
|
HTML
|
|
128
197
|
end
|
|
129
198
|
|
|
130
|
-
def
|
|
199
|
+
def close_icon(size = 16)
|
|
131
200
|
<<~HTML
|
|
132
|
-
<svg width="#{size}" height="#{size}" fill="currentColor"
|
|
133
|
-
<
|
|
134
|
-
<
|
|
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"/>
|
|
135
204
|
</svg>
|
|
136
205
|
HTML
|
|
137
206
|
end
|
|
138
207
|
|
|
139
|
-
def
|
|
208
|
+
def warning_icon(size = 16)
|
|
140
209
|
<<~HTML
|
|
141
|
-
<svg width="#{size}" height="#{size}"
|
|
142
|
-
<path d="
|
|
143
|
-
<
|
|
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"/>
|
|
144
214
|
</svg>
|
|
145
215
|
HTML
|
|
146
216
|
end
|
|
147
|
-
|
|
148
|
-
def open_dialog_script
|
|
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
217
|
end
|
|
157
218
|
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
|
|
@@ -23,6 +23,8 @@ module UltraSettings
|
|
|
23
23
|
|
|
24
24
|
app_config_dir = Rails.root.join(directory)
|
|
25
25
|
app_config_dir.glob("**/*_configuration.rb").each do |file_path|
|
|
26
|
+
next unless file_path.file? && file_path.readable?
|
|
27
|
+
|
|
26
28
|
relative_path = file_path.relative_path_from(app_config_dir).to_s
|
|
27
29
|
class_name = relative_path.chomp(".rb").classify
|
|
28
30
|
unless UltraSettings.added?(class_name)
|
|
@@ -32,5 +34,11 @@ module UltraSettings
|
|
|
32
34
|
end
|
|
33
35
|
end
|
|
34
36
|
end
|
|
37
|
+
|
|
38
|
+
rake_tasks do
|
|
39
|
+
Dir.glob(File.expand_path("tasks/*.rake", __dir__)).each do |rake_file|
|
|
40
|
+
load rake_file
|
|
41
|
+
end
|
|
42
|
+
end
|
|
35
43
|
end
|
|
36
44
|
end
|