Skip to content

Commit

Permalink
Merge pull request #81 from DFE-Digital/custom-i18n-schemas
Browse files Browse the repository at this point in the history
Add support for customising the localisation schema (provided in #68) as part of the
application config.

The configuration allows for the value localisation_schema_fallback to be set, which
provides a default 'base' schema; and localisation_schema_label, localisation_schema_hint,
and localisation_schema_legend to be set which configures labels, hints and legends
respectively.

One slightly-breaking change (from the initial version of localisation functionality) is
that now we're referring to fieldset legends by legend rather than fieldset. This is
more inkeeping with the language used everywhere else in the project and definitely
feels cleaner.
  • Loading branch information
peteryates authored Dec 21, 2019
2 parents 96eb66b + a37bcc1 commit b6b09ac
Show file tree
Hide file tree
Showing 16 changed files with 269 additions and 9 deletions.
32 changes: 32 additions & 0 deletions guide/content/building-blocks/localisation.slim
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,35 @@ section
| Note that despite the <code>text</code> attribute being omitted from
the label options hash, the other display and formatting parameters
can be supplied and work in the normal manner.

== render('/partials/example-fig.*',
caption: "Customising locale structure",
localisation: custom_locale,
code: role_name,
custom_config: custom_locale_config,
raw_config: custom_locale_config_raw,
hide_html_output: true) do

p.govuk-body
| There are many approaches to organising localisation data and while the default
will work for most projects, sometimes a different approach can be beneficial. This
is especially true when working with external localisation agencies or when dealing
with large volumes of copy.

p.govuk-body
| To customise the location of our localisation strings, we can
#{link_to('configure', '/introduction/configuration').html_safe} the schema as
part of the application's initialisation process.

h4.govuk-heading-s Contexts

p.govuk-body
| There are three contexts supported by the form builder: <em>label</em>,
<em>legend</em> and <em>hint</em>. Custom locale schemas are
configured using an array of symbols that match your locale structure.

p.govuk-body
| The special value <code>__context__</code> is used to represent the
current translation context. It will automatically be replaced with
either <em>label</em>, <em>legend</em> or <em>hint</em> when the
translation key is generated.
21 changes: 20 additions & 1 deletion guide/layouts/partials/example-fig.slim
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
- if defined?(custom_config)
- custom_config

figure
figcaption
h3.govuk-heading-m.example-heading id=anchor_id(caption)
Expand All @@ -17,6 +20,19 @@ figure
code.highlight.language-ruby
| #{sample_data}

- if defined?(raw_config)
section
h4.govuk-heading-s.example-subheading.locale Configuration

div.govuk-inset-text
| Place your configuration in an
#{link_to('initializer', rails_initializer_link).html_safe} to ensure
it's loaded every time you start your Rails application.

pre.example-input
code.highlight.language-ruby
| #{raw_config}

- if defined?(localisation)
- I18n.backend.store_translations(:en, YAML.load(localisation))

Expand All @@ -27,7 +43,6 @@ figure
code.highlight.language-yaml
| #{localisation}


section
h4.govuk-heading-s.example-subheading.input Input
pre.example-input
Expand All @@ -49,3 +64,7 @@ figure
- if defined?(sample_data)
- eval(sample_data)
= format_slim(code, f: builder(display_errors), **form_data)


- if defined?(custom_config)
- reset_config
30 changes: 30 additions & 0 deletions guide/lib/examples/localisation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,35 @@ def favourite_kind_of_hat
= f.govuk_text_field :favourite_kind_of_hat, label: { size: 'm' }
SNIPPET
end

def role_name
<<~SNIPPET
= f.govuk_text_field :role, label: { size: 'm' }
SNIPPET
end

def custom_locale
<<~LOCALE
helpers:
label:
person:
role: What role do you play?
copy:
descriptions:
hint:
subdivision:
person:
role: |-
Roles may be achieved or ascribed or they can be accidental
in different situations. An achieved role is a position
that a person assumes voluntarily which reflects personal
skills, abilities, and effort.
LOCALE
end

def reset_config
GOVUKDesignSystemFormBuilder.reset!
end
end
end
4 changes: 4 additions & 0 deletions guide/lib/helpers/link_helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ def dfe_rails_boilerplate_link
'https://github.com/DFE-Digital/govuk-rails-boilerplate'
end

def rails_initializer_link
'https://guides.rubyonrails.org/configuring.html#using-initializer-files'
end

def slim_link
'https://slim-lang.com'
end
Expand Down
3 changes: 2 additions & 1 deletion guide/lib/helpers/person.rb
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ class Person

# localisation
attr_accessor(
:favourite_kind_of_hat
:favourite_kind_of_hat,
:role
)
end
13 changes: 13 additions & 0 deletions guide/lib/setup/example_data.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,15 @@ def laptops_data_raw
DATA
end

def custom_locale_config_raw
<<~CONFIG
GOVUKDesignSystemFormBuilder.configure do |conf|
conf.localisation_schema_fallback = %i(helpers __context__)
conf.localisation_schema_hint = %i(copy descriptions __context__ subdivision)
end
CONFIG
end

# Yes, eval is bad, but when you want to display code in documentation as
# well as run it, it's kind of necessary. Not considering this a security
# threat as it's only used in the guide 👮
Expand All @@ -75,6 +84,10 @@ def primary_colours
def laptops
eval(laptops_data_raw)
end

def custom_locale_config
eval(custom_locale_config_raw)
end
# rubocop:enable Security/Eval

def form_data
Expand Down
37 changes: 36 additions & 1 deletion lib/govuk_design_system_formbuilder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,47 @@
module GOVUKDesignSystemFormBuilder
include ActiveSupport::Configurable

# @!group Defaults

# Default form builder configuration
#
# * +:default_legend_size+ controls the default size of legend text.
# Can be either +xl+, +l+, +m+ or +s+.
#
# * +:default_legend_tag+ controls the default tag that legends are
# wrapped in. Defaults to +h1+.
#
# * +:default_submit_button_text+ sets the value assigned to +govuk_submit+,
# defaults to 'Continue'.
#
# * +:default_submit_button_text+ sets the text used to divide the last radio
# button in radio button fieldsets. As per the GOV.UK Design System spec,
# it defaults to 'or'.
#
# * +:default_error_summary_title+ sets the text used in error summary
# blocks. As per the GOV.UK Design System spec, it defaults to
# 'There is a problem'.
#
# * +:localisation_schema_fallback+ sets the prefix elements for the array
# used to build the localisation string. The final two elements are always
# are the object name and attribute name. The _special_ value +__context__+,
# is used as a placeholder for the context (label, fieldset or hint).
#
# * +:localisation_schema_legend+, +:localisation_schema_hint+ and
# +:localisation_schema_label+ each override the schema root for their
# particular context, allowing them to be independently customised.
# ===
DEFAULTS = {
default_legend_size: 'm',
default_legend_tag: 'h1',
default_submit_button_text: 'Continue',
default_radio_divider_text: 'or',
default_error_summary_title: 'There is a problem'
default_error_summary_title: 'There is a problem',

localisation_schema_fallback: %i(helpers __context__),
localisation_schema_label: nil,
localisation_schema_hint: nil,
localisation_schema_legend: nil
}.freeze

DEFAULTS.keys.each { |k| config_accessor(k) { DEFAULTS[k] } }
Expand Down
2 changes: 1 addition & 1 deletion lib/govuk_design_system_formbuilder/containers/fieldset.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def build_legend
end

def legend_text
[@legend.dig(:text), localised_text('fieldset')].compact.first
[@legend.dig(:text), localised_text(:legend)].compact.first
end

def fieldset_classes
Expand Down
2 changes: 1 addition & 1 deletion lib/govuk_design_system_formbuilder/elements/hint.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def html
def hint_text(supplied)
[
supplied.presence,
localised_text('hint')
localised_text(:hint)
].compact.first
end

Expand Down
2 changes: 1 addition & 1 deletion lib/govuk_design_system_formbuilder/elements/label.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def build_label
end

def label_text(option_text, hidden)
text = [option_text, @value, localised_text('label'), @attribute_name.capitalize].compact.first.to_s
text = [option_text, @value, localised_text(:label), @attribute_name.capitalize].compact.first.to_s

if hidden
tag.span(text, class: %w(govuk-visually-hidden))
Expand Down
22 changes: 21 additions & 1 deletion lib/govuk_design_system_formbuilder/traits/localisation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,27 @@ def localised_text(context)
def localisation_key(context)
return nil unless @object_name.present? && @attribute_name.present?

['helpers', context, @object_name, @attribute_name].join('.')
schema(context)
end

def schema(context)
schema_root(context)
.push(@object_name, @attribute_name)
.map { |e| e == :__context__ ? context : e }
.join('.')
end

def schema_root(context)
contextual_schema = case context
when :legend
config.localisation_schema_legend
when :hint
config.localisation_schema_hint
when :label
config.localisation_schema_label
end

(contextual_schema || config.localisation_schema_fallback).dup
end
end
end
Expand Down
83 changes: 83 additions & 0 deletions spec/govuk_design_system_formbuilder/builder/configuration_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -163,5 +163,88 @@
end
end
end

describe 'localisation configuration' do
let(:args) { [method, attribute] }
let(:attribute) { :name }
let(:method) { :govuk_text_field }
subject { builder.send(*args) }

context 'with a custom fallback localisation schema root' do
let(:localisations) { { en: YAML.load_file('spec/support/locales/custom_fallback.en.yaml') } }
let(:label_text) { I18n.t(%i(my_shiny_new_app label widgets person name).join('.')) }

before do
GOVUKDesignSystemFormBuilder.configure do |conf|
conf.localisation_schema_fallback = %i(my_shiny_new_app __context__ widgets)
end
end

specify 'should use the configured localisation fallback schema' do
with_localisations(localisations) do
expect(subject).to have_tag('label', text: label_text)
end
end
end

context 'with custom contextual localisation schema roots' do
let(:localisations) { { en: YAML.load_file('spec/support/locales/custom_contexts.en.yaml') } }

context 'labels' do
let(:label_text) { I18n.t(%i(a long convoluted schema nested_far_far label too_inconsistently person name).join('.')) }

before do
GOVUKDesignSystemFormBuilder.configure do |conf|
conf.localisation_schema_label = %i(a long convoluted schema nested_far_far __context__ too_inconsistently)
conf.localisation_schema_fallback = %i(unused)
end
end

specify 'should use the configured localisation schema for labels' do
with_localisations(localisations) do
expect(subject).to have_tag('label', text: label_text)
end
end
end

context 'hints' do
let(:hint_text) { I18n.t(%i(a long convoluted schema hint nested_far_too_deeply person name).join('.')) }

before do
GOVUKDesignSystemFormBuilder.configure do |conf|
conf.localisation_schema_hint = %i(a long convoluted schema __context__ nested_far_too_deeply)
conf.localisation_schema_fallback = %i(unused)
end
end

specify 'should use the configured localisation schema for hints' do
with_localisations(localisations) do
expect(subject).to have_tag('span', text: hint_text, with: { class: 'govuk-hint' })
end
end
end

context 'legends' do
let(:attribute) { :projects }
let(:method) { :govuk_collection_check_boxes }
let(:legend_text) { I18n.t(%i(a long convoluted schema legend nested_far_too_deeply person projects).join('.')) }

subject { builder.send(method, attribute, projects, :id, :name) }

before do
GOVUKDesignSystemFormBuilder.configure do |conf|
conf.localisation_schema_legend = %i(a long convoluted schema __context__ nested_far_too_deeply)
conf.localisation_schema_fallback = %i(unused)
end
end

specify 'should use the configured localisation schema for legends' do
with_localisations(localisations) do
expect(subject).to have_tag('legend', text: legend_text, with: { class: 'govuk-fieldset__legend' })
end
end
end
end
end
end
end
18 changes: 18 additions & 0 deletions spec/support/locales/custom_contexts.en.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
a:
long:
convoluted:
schema:
legend:
nested_far_too_deeply:
person:
projects: The things that keep you busiest at work
hint:
nested_far_too_deeply:
person:
name: It should say it on your birth certificate
nested_far_far:
label:
too_inconsistently:
person:
name: Your name, what is it?
projects: What do you do at work?
5 changes: 5 additions & 0 deletions spec/support/locales/custom_fallback.en.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
my_shiny_new_app:
label:
widgets:
person:
name: Who goes there?
2 changes: 1 addition & 1 deletion spec/support/locales/sample.en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ helpers:
cv: Tell us about your employment history
favourite_colour: What is your favourite colour?
photo: Upload a recent passport photo
fieldset:
legend:
person:
favourite_colour: To which colours are you most partial?
born_on: What is your date of birth?
Expand Down
2 changes: 1 addition & 1 deletion spec/support/shared/shared_localisation_examples.rb
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
let(:localisations) { LOCALISATIONS }

context 'localising when no text is supplied' do
let(:expected_legend) { I18n.translate("helpers.fieldset.person.#{attribute}") }
let(:expected_legend) { I18n.translate("helpers.legend.person.#{attribute}") }
subject { builder.send(*args) { arbitrary_html_content } }

specify 'should set the legend from the locales' do
Expand Down

0 comments on commit b6b09ac

Please sign in to comment.