test(i18n): cover German locale completeness

This commit is contained in:
sure-admin
2026-08-04 22:23:04 +00:00
parent e7880948b8
commit e9e1d4a26a
22 changed files with 194 additions and 23 deletions
+3
View File
@@ -65,12 +65,15 @@ de:
authorize: 'Autorisieren'
deny: 'Ablehnen'
error:
go_back: 'Zurück'
title: 'Ein Fehler ist aufgetreten'
new:
title: 'Autorisierung erforderlich'
prompt: '%{client_name} Zugriff auf dein Konto erlauben?'
able_to: 'Diese Anwendung kann Folgendes tun'
show:
authorization_code_label: 'Autorisierungscode:'
copy_instructions: 'Kopiere diesen Code und füge ihn in der Anwendung ein.'
title: 'Autorisierungscode'
form_post:
title: 'Dieses Formular absenden'
+1 -1
View File
@@ -6,7 +6,7 @@ de:
importing_wallets: Wallets werden von CoinStats importiert...
checking_configuration: Wallet-Konfiguration wird geprüft...
wallets_need_setup:
one: "1 Wallet muss eingerichtet werden..."
one: "%{count} Wallet muss eingerichtet werden..."
other: "%{count} Wallets müssen eingerichtet werden..."
processing_holdings: Bestände werden verarbeitet...
calculating_balances: Salden werden berechnet...
+2 -3
View File
@@ -4,8 +4,8 @@ de:
attributes:
user:
email: E-Mail
family: Familie
family_id: Familie
family: "%{moniker}"
family_id: "%{moniker}"
first_name: Vorname
last_name: Nachname
password: Passwort
@@ -18,4 +18,3 @@ de:
cannot_deactivate_admin_with_other_users: Admin kann das Konto nicht löschen, solange noch andere Benutzer vorhanden sind Bitte zuerst alle Mitglieder löschen
profile_image:
invalid_file_size: Dateigröße muss kleiner als %{max_megabytes}MB sein
@@ -17,6 +17,7 @@ de:
one: "%{count} Konto verfügbar"
other: "%{count} Konten verfügbar"
select_all: Alle auswählen
historical_import: Verlaufsimport
import_selected: Ausgewählte importieren
cancel: Abbrechen
creating: Importiere...
@@ -110,6 +110,7 @@ de:
creating_accounts: Konten werden angelegt …
header_subtitle: Wähl die passende Kontoart für deine importierten Konten
historical_data_range: 'Zeitraum der Historie:'
psd2_savings_notice: 'Hinweis: Einige regulierte französische Sparkonten (Livret A, PEL, LEP, LDDS) sind über Open Banking (PSD2) nur eingeschränkt oder gar nicht verfügbar. Wenn ein Sparkonto fehlt, kannst du es manuell hinzufügen.'
sync_start_date_help: Wähl, wie weit zurück die Transaktionshistorie geholt werden soll. Verfügbar sind maximal 2 Jahre.
sync_start_date_label: 'Transaktionen synchronisieren ab:'
title: Enable-Banking-Konten einrichten
+5
View File
@@ -94,11 +94,13 @@ de:
heading: Archiviert
chips:
all: Alle
aria: Ziele nach Status filtern
behind: Hinter Plan
completed: Erreicht
no_target_date: Offen
on_track: Im Plan
paused: Pausiert
chips_aria: Ziele nach Status filtern
empty_filtered: Kein Ziel passt.
goals_section:
heading: Ziele
@@ -152,6 +154,9 @@ de:
pause:
invalid_transition: Dieses Ziel lässt sich in seinem aktuellen Zustand nicht pausieren.
success: Ziel pausiert.
perform_transition!:
invalid_transition: Ziel kann nicht in diesen Status wechseln.
success: Ziel aktualisiert.
reopen:
invalid_transition: Dieses Ziel lässt sich in seinem aktuellen Zustand nicht wieder öffnen.
success: Ziel wieder geöffnet.
@@ -42,12 +42,12 @@ de:
fields:
api_token:
label: "API-Token"
description: "Ihr schreibgeschützter API-Token aus dem IndexaCapital Dashboard"
description: "Dein schreibgeschützter API-Token aus dem IndexaCapital Dashboard"
placeholder_new: "API-Token hier einfügen"
placeholder_update: "Neuen API-Token zum Aktualisieren eingeben"
username:
label: "Benutzername"
description: "Ihr IndexaCapital Benutzername/E-Mail"
description: "Dein IndexaCapital Benutzername/E-Mail"
placeholder_new: "Benutzername hier einfügen"
placeholder_update: "Neuen Benutzernamen zum Aktualisieren eingeben"
document:
@@ -57,7 +57,7 @@ de:
placeholder_update: "Neue Dokument-ID zum Aktualisieren eingeben"
password:
label: "Passwort"
description: "Ihr IndexaCapital Passwort"
description: "Dein IndexaCapital Passwort"
placeholder_new: "Passwort hier einfügen"
placeholder_update: "Neues Passwort zum Aktualisieren eingeben"
@@ -3,6 +3,6 @@ de:
invitation_mailer:
invite_email:
accept_button: Einladung annehmen
body: "%{inviter} hat dich eingeladen, der Familie %{family} auf %{product_name} beizutreten."
body: "%{inviter} hat dich eingeladen, der %{family} %{moniker} auf %{product_name} beizutreten."
expiry_notice: Diese Einladung läuft in %{days} Tagen ab.
greeting: Willkommen bei %{product_name}!
+1 -1
View File
@@ -24,5 +24,5 @@ de:
role_label: Rolle
role_member: Mitglied
submit: Einladung senden
subtitle: Lad jemanden in deine %{moniker} ein
subtitle: Lad jemanden in dein %{moniker}-Konto auf %{product_name} ein
title: Jemanden einladen
+2 -2
View File
@@ -8,7 +8,7 @@ de:
description: "Mit %{name} verknüpfen"
create:
default_name: Kraken
success: Erfolgreich mit Kraken verbunden. Ihr Börsenkonto wird synchronisiert.
success: Erfolgreich mit Kraken verbunden. Dein Börsenkonto wird synchronisiert.
update:
success: Kraken-Verbindung erfolgreich aktualisiert.
destroy:
@@ -66,7 +66,7 @@ de:
status_never: Noch nie synchronisiert
delete: Löschen
no_accounts_title: Keine Konten gefunden
no_accounts_message: Ihr Kraken-Börsenkonto erscheint hier nach der Synchronisierung.
no_accounts_message: Dein Kraken-Börsenkonto erscheint hier nach der Synchronisierung.
setup_needed: Konto bereit zum Import
setup_description: Importier diese Kraken-Verbindung als Krypto-Börsenkonto.
setup_action: Konto importieren
@@ -20,6 +20,7 @@ de:
other: "%{count} Konten mit leerem Namen können nicht verknüpft werden"
link_failed: Konten konnten nicht verknüpft werden
no_accounts_selected: Bitte wähle mindestens ein Konto aus
no_api_key: Der Lunch-Flow-API-Key ist nicht eingerichtet. Richte ihn in den Einstellungen ein.
partial_invalid: "%{created_count} Konto/Konten verknüpft, %{already_linked_count} waren bereits verknüpft, %{invalid_count} hatten ungültige Namen"
partial_success: "%{created_count} Konto/Konten erfolgreich verknüpft. %{already_linked_count} Konto/Konten waren bereits verknüpft: %{already_linked_names}"
success:
@@ -50,6 +51,7 @@ de:
link_accounts: Ausgewählte Konten verknüpfen
no_accounts_found: Keine Konten gefunden. Bitte überprüfe deine API-Key-Konfiguration.
no_api_key: LunchFlow-API-Schlüssel ist nicht konfiguriert. Bitte konfiguriere ihn in den Einstellungen.
no_credentials_configured: Richte zuerst deine Lunch-Flow-Zugangsdaten in den Anbietereinstellungen ein.
no_name_placeholder: "(Kein Name)"
title: LunchFlow-Konten auswählen
select_existing_account:
@@ -63,6 +65,7 @@ de:
no_account_specified: Kein Konto angegeben
no_accounts_found: Keine LunchFlow-Konten gefunden. Bitte überprüfe deine API-Key-Konfiguration.
no_api_key: LunchFlow-API-Schlüssel ist nicht konfiguriert. Bitte konfiguriere ihn in den Einstellungen.
no_credentials_configured: Richte zuerst deine Lunch-Flow-Zugangsdaten in den Anbietereinstellungen ein.
no_name_placeholder: "(Kein Name)"
title: "%{account_name} mit LunchFlow verknüpfen"
link_existing_account:
@@ -72,6 +75,7 @@ de:
lunchflow_account_already_linked: Dieses LunchFlow-Konto ist bereits mit einem anderen Konto verknüpft
lunchflow_account_not_found: LunchFlow-Konto nicht gefunden
missing_parameters: Erforderliche Parameter fehlen
no_api_key: Der Lunch-Flow-API-Key ist nicht eingerichtet. Richte ihn in den Anbietereinstellungen ein.
success: "%{account_name} erfolgreich mit LunchFlow verknüpft"
setup_accounts:
account_type_label: "Kontotyp:"
+2 -2
View File
@@ -19,9 +19,9 @@ de:
merge: Händler zusammenführen
title: Händler
family_title: Händler deiner %{moniker}
family_empty: Noch keine Händler der Familie vorhanden
family_empty: Noch keine Händler der %{moniker} vorhanden
provider_title: Anbieter-Händler
provider_empty: Noch keine Anbieter-Händler mit dieser Familie verbunden
provider_empty: Noch keine Anbieter-Händler mit dieser %{moniker} verbunden
provider_read_only: Anbieter-Händler werden von deinen verbundenen Instituten synchronisiert. Sie lassen sich hier nicht bearbeiten.
provider_info: Diese Händler wurden automatisch von deinen Bankverbindungen oder der KI erkannt. Du kannst sie bearbeiten, um deine eigene Kopie zu erstellen, oder sie entfernen, um sie von deinen Transaktionen zu trennen.
unlinked_title: Kürzlich getrennt
@@ -49,7 +49,11 @@ de:
loading_message: Questrade-Konten werden geladen …
loading_title: Wird geladen
panel:
disconnect: Trennen
disconnect_confirm: Diese Questrade-Verbindung wirklich trennen? Dadurch werden alle synchronisierten Kontodaten entfernt.
eyebrow: Questrade
field_descriptions: 'Erläuterung der Felder:'
help_text: Sure nutzt diese Zugangsdaten nur, um deine Questrade-Konten, Bestände und Aktivitäten zu synchronisieren.
fields:
api_server:
description: Dein Questrade-API-Server
@@ -68,6 +72,13 @@ de:
setup_instructions: 'So richtest du es ein:'
status_configured_html: Eingerichtet und einsatzbereit. Im Reiter <a href="%{accounts_path}" class="link">Konten</a> verwaltest und richtest du Konten ein.
status_not_configured: Nicht eingerichtet
steps:
s1_html: Öffne dein <strong>Questrade-Dashboard</strong>.
s2: Erstelle oder kopiere deine API-Zugangsdaten.
s3_html: Füge den <strong>Refresh Token</strong> und den API-Server unten ein.
s4: Speichere die Verbindung.
s5: Starte einen Sync, um Konten zu finden.
s6: Richte die gefundenen Konten im Konten-Reiter ein.
step_1: Öffne dein Questrade-Dashboard und hol dir deine Zugangsdaten
step_2: Trag die Zugangsdaten unten ein und klick auf Speichern
step_3: Nach erfolgreicher Verbindung richtest du im Reiter „Konten“ die neuen Konten ein
@@ -115,15 +126,21 @@ de:
all_accounts_already_linked: Alle Questrade-Konten sind bereits verknüpft
api_error: 'API-Fehler: %{message}'
balance_label: 'Saldo:'
buttons:
cancel: Abbrechen
link: Verknüpfen
cancel: Abbrechen
cancel_button: Abbrechen
configure_name_in_provider: Import nicht möglich hinterleg den Kontonamen in Questrade
connect_hint: Verbinde ein Questrade-Konto, um den automatischen Abgleich zu nutzen.
description: Wähl ein Questrade-Konto, das mit diesem Konto verknüpft werden soll. Transaktionen werden automatisch abgeglichen und Dubletten entfernt.
dialog_title: Mit einem Questrade-Konto verknüpfen
header: Mit Questrade verknüpfen
link_account: Konto verknüpfen
link_button: Dieses Konto verknüpfen
linking_label: Verknüpft wird
linking_to: 'Wird verknüpft mit:'
none_available: Keine unverknüpften Questrade-Konten verfügbar.
no_account_specified: Kein Konto angegeben
no_accounts: Keine unverknüpften Questrade-Konten gefunden.
no_accounts_found: Keine Questrade-Konten gefunden. Prüf die Konfiguration deines API-Keys.
@@ -132,9 +149,11 @@ de:
no_name_placeholder: "(kein Name)"
settings_link: Zu den Anbietereinstellungen
subtitle: Ein Questrade-Konto wählen
page_title: Questrade-Konto verknüpfen
title: "%{account_name} mit Questrade verknüpfen"
setup_accounts:
account_type_label: 'Kontoart:'
all_linked: Alle Questrade-Konten sind bereits eingerichtet.
account_types:
credit_card: Kreditkarte
crypto: Krypto-Konto
@@ -149,6 +168,10 @@ de:
all_accounts_linked: Alle deine Questrade-Konten sind bereits eingerichtet.
api_error: 'API-Fehler: %{message}'
balance: Saldo
buttons:
cancel: Abbrechen
create: Konten anlegen
done: Fertig
cancel: Abbrechen
choose_account_type: 'Wähl für jedes Questrade-Konto die passende Kontoart:'
create_accounts: Konten anlegen
@@ -156,6 +179,8 @@ de:
creating_accounts: Konten werden angelegt …
fetch_failed: Konten ließen sich nicht abrufen
historical_data_range: 'Zeitraum der Historie:'
dialog_title: Deine Questrade-Konten einrichten
import_as: Importieren als
import_selected: Gewählte Konten importieren
instructions: Wähl die Konten, die du aus Questrade importieren willst. Du kannst mehrere auswählen.
no_accounts: Aus dieser Questrade-Verbindung gibt es keine unverknüpften Konten.
@@ -163,6 +188,7 @@ de:
no_api_key: Der Questrade-API-Key ist nicht eingerichtet. Prüf deine Verbindungseinstellungen.
select_all: Alle auswählen
subtitle: Wähl die passende Kontoart für deine importierten Konten
page_title: Questrade-Konten einrichten
subtype_labels:
credit_card: ''
crypto: ''
@@ -203,6 +229,10 @@ de:
sync_start_date_help: Wähl, wie weit zurück die Transaktionshistorie geholt werden soll.
sync_start_date_label: 'Transaktionen synchronisieren ab:'
title: Questrade-Konten einrichten
types:
depository: Giro- oder Sparkonto
investment: Investmentkonto
skip: Überspringen
setup_required:
go_to_provider_settings: Zu den Anbietereinstellungen
not_configured_description: Leg deine Questrade-Verbindung in den Anbietereinstellungen an und komm dann hierher zurück, um Konten zu verknüpfen.
+1 -1
View File
@@ -15,7 +15,7 @@ de:
success: Du hast dich erfolgreich registriert
new:
invitation_message: "%{inviter} hat dich eingeladen als %{role} beizutreten"
join_family_title: "%{family} beitreten"
join_family_title: "%{family} %{moniker} beitreten"
role_admin: Administrator
role_guest: Gast
role_member: Mitglied
+1
View File
@@ -2,6 +2,7 @@
de:
reports:
index:
period_aria: Berichtszeitraum
title: Berichte
subtitle: Umfassende Einblicke in deine finanzielle Situation
export: CSV exportieren
+5 -1
View File
@@ -23,7 +23,7 @@ de:
payments:
show:
page_title: Zahlung
subscription_subtitle: Aktualisiere dein Abonnement und deine Zahlungsdetails
subscription_subtitle: Aktualisiere deine Kreditkartendaten
subscription_title: Abonnement verwalten
choose_level: Stufe wählen
contributions_note: Beiträge zu %{product_name} erscheinen hier.
@@ -177,6 +177,10 @@ de:
encryption_warning:
generate: 'Einen Satz erzeugst du mit: bin/rails db:encryption:init'
intro: 'Sensible Daten wie API-Keys, Anbieter-Tokens, MFA-Geheimnisse und personenbezogene Angaben liegen unverschlüsselt auf der Platte. Setze diese Schlüssel in deinen Umgebungsvariablen oder Rails-Credentials, um die Verschlüsselung einzuschalten:'
keys:
- ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY
- ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY
- ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT
title: Verschlüsselungsschlüssel fehlen
settings_nav:
accounts_label: Konten
+1
View File
@@ -34,6 +34,7 @@ de:
message: Neue Daten verfügbar
refresh: Aktualisieren
transaction_tabs:
aria: Transaktionstyp
expense: Ausgabe
income: Einnahme
transfer: Überweisung
+1 -1
View File
@@ -86,7 +86,7 @@ de:
all_skipped: "Alle Konten wurden übersprungen. Es wurden keine Konten erstellt."
no_accounts: "Keine Konten zum Einrichten."
success:
one: "Ein SimpleFIN-Konto erfolgreich erstellt! Deine Transaktionen und Positionen werden im Hintergrund importiert."
one: "%{count} SimpleFIN-Konto erfolgreich erstellt! Deine Transaktionen und Positionen werden im Hintergrund importiert."
other: "%{count} SimpleFIN-Konten erfolgreich erstellt! Deine Transaktionen und Positionen werden im Hintergrund importiert."
stale_accounts_processed: "Veraltete Konten: %{deleted} gelöscht, %{moved} verschoben."
stale_accounts_errors:
+4 -4
View File
@@ -30,7 +30,7 @@ en:
label: "SimpleFIN Setup Token:"
placeholder: "Paste your SimpleFIN setup token here..."
help_text: "The token should be a long string starting with letters and numbers"
title: Update SimpleFin Connection
title: Update SimpleFIN Connection
header_subtitle: Get a new setup token to reconnect your SimpleFIN account
connection_needs_update: "Your SimpleFIN connection needs to be updated:"
step_1_html: "Visit <a href=\"https://bridge.simplefin.org/simplefin/create\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"text-link hover:text-link underline\">SimpleFIN Bridge</a> to create a new setup token"
@@ -39,9 +39,9 @@ en:
update: Update
cancel: Cancel
setup_accounts:
title: Set Up Your SimpleFin Accounts
title: Set Up Your SimpleFIN Accounts
header_subtitle: Choose the correct account types for your imported accounts
choose_account_type: "Choose the correct account type for each SimpleFin account:"
choose_account_type: "Choose the correct account type for each SimpleFIN account:"
account_type_checking_savings: Checking or Savings
account_type_checking_savings_desc: Regular bank accounts
account_type_credit_card: Credit Card
@@ -53,7 +53,7 @@ en:
account_type_other_asset: Other Asset
account_type_other_asset_desc: Everything else
transaction_history_title: "Transaction History:"
transaction_history_description_html: "SimpleFin typically provides <strong>60-90 days</strong> of transaction history, depending on your bank. After initial setup, new transactions will sync automatically going forward. Historical data availability varies by institution and account type."
transaction_history_description_html: "SimpleFIN typically provides <strong>60-90 days</strong> of transaction history, depending on your bank. After initial setup, new transactions will sync automatically going forward. Historical data availability varies by institution and account type."
account_type_label: "Account Type:"
create_accounts: Create Accounts
creating_accounts: Creating Accounts...
+5 -3
View File
@@ -14,6 +14,8 @@ de:
valuations: Wert
value: Wert
new:
amount: Betrag
submit: Kontostand hinzufügen
title: Neuer Kontostand
show:
amount: Betrag
@@ -38,8 +40,8 @@ de:
title: Saldo aktualisieren
update: Aktualisieren
confirmation_contents:
action_set: 'Neu:'
action_update: 'Geändert:'
action_set: gesetzt
action_update: aktualisiert
account_balance: Kontostand
asset_value: Vermögenswert
balance: Saldo
@@ -53,7 +55,7 @@ de:
"on": am
property_value: Immobilienwert
recalculate_notice: Alle späteren Transaktionen und Salden werden auf Basis dieser %{change_or_update} neu berechnet.
this_will: Damit wird der Kontowert zum
this_will: Damit wird der Kontowert %{action_verb} am
to: auf
to_colon: 'auf:'
total_account_value: Gesamtwert des Kontos
+1
View File
@@ -84,6 +84,7 @@ de:
description: Wähl das Wise-Währungsguthaben, das mit diesem Konto verknüpft werden soll. Transaktionen werden automatisch abgeglichen.
link_account: Guthaben verknüpfen
no_accounts_found: Keine unverknüpften Wise-Guthaben gefunden.
not_found: Wise-Guthaben nicht gefunden.
title: "%{account_name} mit Wise verknüpfen"
select_profiles:
already_connected_label: Bereits verbunden
+119
View File
@@ -1,13 +1,62 @@
require "i18n/tasks"
require "pathname"
require "set"
require "yaml"
# We're currently skipping some i18n tests to speed up development. Eventually, we'll make a dedicated
# project for getting i18n working. More details on that here:
# https://github.com/maybe-finance/maybe/issues/1225
class I18nTest < ActiveSupport::TestCase
GERMAN_COVERAGE_GLOBS = [
"config/locales/breadcrumbs/*.yml",
"config/locales/doorkeeper.*.yml",
"config/locales/mailers/**/*.yml",
"config/locales/models/**/*.yml",
"config/locales/views/**/*.yml"
]
def setup
@i18n = I18n::Tasks::BaseTask.new
end
def test_german_locale_files_parse
german_locale_paths.each do |path|
assert_nothing_raised do
YAML.load_file(path, aliases: true)
end
end
end
def test_german_app_locale_covers_english_keys
missing_keys = english_app_locale_leaves.keys.filter_map do |key|
key unless translation_present?(german_app_locale_tree, key)
end
assert_empty missing_keys.sort, "Missing German i18n keys:\n#{missing_keys.sort.join("\n")}"
end
def test_german_app_locale_interpolations_match_english
mismatches = english_app_locale_leaves.filter_map do |key, english_value|
next unless english_value.is_a?(String)
german_value = translation_at(german_app_locale_tree, key)
next if german_value.nil?
expected_interpolations = interpolation_keys(english_value)
german_values =
if german_value.is_a?(Hash)
flatten_leaves(german_value).values.grep(String)
else
[ german_value ]
end
bad_values = german_values.reject { |value| interpolation_keys(value) == expected_interpolations }
"#{key}: expected #{expected_interpolations.to_a.sort}, got #{bad_values.map { |value| interpolation_keys(value).to_a.sort }.uniq}" if bad_values.any?
end
assert_empty mismatches, "German i18n interpolation mismatches:\n#{mismatches.join("\n")}"
end
def test_no_missing_keys
skip "Skipping missing keys test"
missing_keys = @i18n.missing_keys(locales: [ :en ])
@@ -38,4 +87,74 @@ class I18nTest < ActiveSupport::TestCase
"Please run `i18n-tasks check-consistent-interpolations' to show them"
assert_empty inconsistent_interpolations, error_message
end
private
def english_app_locale_leaves
@english_app_locale_leaves ||= flatten_leaves(english_app_locale_tree)
end
def english_app_locale_tree
@english_app_locale_tree ||= locale_tree("en")
end
def german_app_locale_tree
@german_app_locale_tree ||= locale_tree("de")
end
def german_locale_paths
@german_locale_paths ||= locale_paths.select { |path| path.basename.to_s.match?(/(^|[._-])de\.yml\z/) }
end
def locale_tree(locale)
locale_paths.each_with_object({}) do |path, tree|
data = YAML.load_file(path, aliases: true) || {}
deep_merge!(tree, data.fetch(locale, {}))
end
end
def locale_paths
@locale_paths ||= GERMAN_COVERAGE_GLOBS.flat_map { |glob| Pathname.pwd.glob(glob) }.uniq
end
def deep_merge!(target, source)
source.each do |key, value|
if target[key].is_a?(Hash) && value.is_a?(Hash)
deep_merge!(target[key], value)
else
target[key] = value
end
end
target
end
def flatten_leaves(value, prefix = [], result = {})
case value
when Hash
value.each { |key, child| flatten_leaves(child, prefix + [ key.to_s ], result) }
else
result[prefix.join(".")] = value
end
result
end
def translation_at(tree, key)
key.split(".").reduce(tree) do |current, segment|
return nil unless current.is_a?(Hash) && current.key?(segment)
current[segment]
end
end
def translation_present?(tree, key)
value = translation_at(tree, key)
return false if value.nil?
true
end
def interpolation_keys(value)
value.scan(/%\{([^}]+)\}/).flatten.to_set
end
end