Skip to content

Commit

Permalink
Merge pull request #177 from SFDigitalServices/release-7.2.0
Browse files Browse the repository at this point in the history
Release 7.2.0
  • Loading branch information
shawnbot authored Feb 2, 2021
2 parents f7a91b7 + 6729e21 commit e0dc789
Show file tree
Hide file tree
Showing 9 changed files with 248 additions and 13 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ This is the [Form.io] theme for [sf.gov](https://sf.gov).
* [Formio patches](docs/patches.md)
* [Form options](docs/patches.md#form-options)
* [Form upgrades](docs/patches.md#form-upgrades)
* [Autocomplete components](docs/autocomplete.md)
* [Icons](docs/patches.md#icons)
* [Declarative actions](docs/patches.md#declarative-actions)
* [Localization](docs/localization.md#readme)
Expand Down
44 changes: 44 additions & 0 deletions docs/autocomplete.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
---
title: Autocomplete components
---

This theme _disables_ the default autocomplete functionality of formio.js's
built-in [select component] and only enables it if the component has an
`autocomplete` tag in the "Field tags" input of the "API" tab on the form.io portal:

![](https://help.form.io/assets/img/api1.png)

## Translation
We patch the `customOptions` of every autocomplete component at runtime to
include translated (or, at least, translatable) UI strings with predictable
keys that can be customized either globally (all autocomplete components in a
single form) or for each instance. These are the options that [Choices.js] lets
us localize:

| Option | English | Notes
:--- | :--- | :---
[`searchPlaceholderValue`](https://github.com/jshjohnson/Choices#searchplaceholdervalue) | "Type to search" | The placeholder text of the search input
[`noResultsText`](https://github.com/jshjohnson/Choices#noresultstext) | "No results found" |
[`itemSelectText`](https://github.com/jshjohnson/Choices#itemselecttext) | _none_ | Displayed on the right side of the dropdown alongside the currently highlighted option
[`maxItemText`](https://github.com/jshjohnson/Choices#maxitemtext) | <nobr>"Only {{count}} values can be added"</nobr> | The `{{count}}` placeholder is substituted with the [maxItemCount](https://github.com/jshjohnson/Choices#maxitemcount) option.
[`noChoicesText`](https://github.com/jshjohnson/Choices#nochoicestext) | "No choices to choose from" | This should only show up if we haven't provided any static options or have a dynamic select driven by an API that doesn't return any results.
[`addItemText`](https://github.com/jshjohnson/Choices#additemtext) | _none_ | ⚠️ I **think** that this option is only applicable when freeform values are allowed. If so, this shows up when you've typed a search string as a prompt, e.g. "Press Enter to add **{{value}}**", where `{{value}}` is substituted with the "backend" value of the option.

To translate these for all autocomplete components in your form, you'll need to
prefix each option with the `autocomplete.` prefix. So, to translate the text
that shows up when no items match the typed value, you would add translations
for the `autocomplete.noResultsText` Phrase key.

You can also translate these strings for individual components by prefixing
`autcomplete.{option}` with the component's key and a `.`. For instance, if you
had an autocomplete component with the key `yourJob` and wanted to translate
the placeholder text, you would add translations for the
`yourJob.autocomplete.searchPlaceholderValue` Phrase key.

The default translations for the `autocomplete.*` keys live in this repo's
[i18n directory] and can be updated from the [generic strings Phrase project].

[select component]: https://help.form.io/userguide/form-components/#select
[choices.js]: https://github.com/jshjohnson/Choices#readme
[i18n directory]: https://github.com/SFDigitalServices/formio-sfds/tree/main/src/i18n
[generic strings phrase project]: https://app.phrase.com/accounts/city-county-of-san-francisco/projects/form-io-generic-strings
75 changes: 71 additions & 4 deletions docs/localization.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,50 @@ aware of:
- [Translate generic strings](#translate-generic-strings-content)
- [Update generic string translations](#update-generic-string-translations-engineering)
- [Translate a specific form](#translate-a-form)
- [Manually adding translations](#manual-translations)

But first, it's important to understand how the whole thing works!

## How it works
There are a bunch of moving pieces involved in coordinating translations
between form.io, Phrase, and our theme:

- The formio.js library that we extend and patch with this theme uses a
localization tool called [i18next], and passes most of the "strings"
(individual chunks of text content, like the text of the "Next" and
"Previous" buttons, or the label of each form component) through its
translation function, `t()`.

- Formio.js accepts an `i18n` option that can include translations. Its shape
is an object with language codes at the top level, and translations below. To
translate the "Next" button text in page navigation to Spanish, we would pass:

```js
{
es: {
Next: 'Siguiente'
}
}
```

- In our templates we call the `t()` function with automatically generated IDs
("keys") for each unique string. The naming scheme is the component's key and
the "path" of the string within the component, separated by `.`. For
instance, the string ID for the label of a component with the key `email`
would be `email.label`. The `content` field of an HTML element `intro` could
be localized with the `intro.content` string.

- We patch several of the third-party libraries that formio.js uses to render
more complex components:

1. We import the Spanish and Chinese translations of [Flatpickr] directly
to localize date and time pickers. These are not customizable right now.

1. We patch the `customOptions` of each [autocomplete component](./autocomplete.md#translation)
to translate UI strings in [Choices.js].

## Translate generic strings (content)
1. Translate the strings in the [generic strings Phrase project](https://app.phrase.com/accounts/city-county-of-san-francisco/projects/form-io-generic-strings)
1. Translate the strings in the [generic strings Phrase project]
1. Ask somebody with access to this repo to pull the new translations (see below)

## Update generic string translations (engineering)
Expand Down Expand Up @@ -45,7 +86,7 @@ Before you can translate a form, you'll need to do some one-time setup in both P
1. In the <kbd>Custom Properties</kbd> section, add a new entry with `phraseProjectId` in the "Key" field and the Phrase project ID that you copied in the "Value" field:

> ![image of the custom properties in form.io](https://user-images.githubusercontent.com/113896/88114083-fa527780-cb67-11ea-98a1-b85273db617a.png)
1. Visit [/api/strings?formUrl=`<URL>`](https://formio-sfds.vercel.app/api/strings?formUrl=<URL>) where `<URL>` is your form.io data source URL
1. Save the JSON to your computer
1. Upload the JSON to your Phrase project:
Expand Down Expand Up @@ -79,7 +120,33 @@ Please use [semantic versioning conventions](https://semver.org) to track the ty
* Minor versions (`1.0.1``1.1.0`) when new keys and/or translations are added
* Major versions (`1.0.0``2.0.0`) when keys are deleted


### Manual translations
Because of how automatically generated translation keys (the unique IDs of each
translatable string) are generated automatically and how the API that
"extracts" strings from each form works, there may be some situations that call
for manually adding strings in Phrase. Here are some tips:

1. First, try adding a string with a key in the form `{component}.{path}`, where
`{component}` is the component key and `{path}` is the "path" of the
component field that you're trying to translate: `label`, `description`, etc.

2. If that doesn't work, please [file an issue]!

3. If your form has been heavily modified on form.io, it's possible that string
keys no longer map to the right components. Try re-generating the form
strings and importing the JSON into Phrase again.

4. If all else fails, you should still be able to add keys for the English
string and translate those. For instance, if a bug is preventing the label
of a field with the label "Your address" from translating, you should still
be able to target it by adding translations for a "Your address" Phrase key.

When in doubt, drop into **#topic-translations** on Slack and ask for help! 💪

[Phrase]: https://phrase.com
[Phrase in-context editor]: https://help.phrase.com/help/set-up-in-context-editor
[phrase]: https://phrase.com
[phrase in-context editor]: https://help.phrase.com/help/set-up-in-context-editor
[choices.js]: https://github.com/jshjohnson/Choices#readme
[generic strings phrase project]: https://app.phrase.com/accounts/city-county-of-san-francisco/projects/form-io-generic-strings
[flatpickr]: https://flatpickr.js.org/
[file an issue]: https://github.com/SFDigitalServices/formio-sfds/issues/new
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "formio-sfds",
"version": "7.1.0",
"version": "7.2.0",
"description": "The Form.io theme for sf.gov",
"module": "src/index.js",
"browser": "dist/formio-sfds.standalone.js",
Expand Down
72 changes: 72 additions & 0 deletions src/examples.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,78 @@
description: And this is the description
tooltip: And this is the tooltip

- id: autocomplete
title: Autocomplete
form:
components:
- type: select
key: job
label: Your job
tags:
- autocomplete
data:
values:
- label: Healthcare worker
value: healthCareWorker
- label: Emergency services
value: emergencyServices
- label: Food and agriculture
value: foodAndAgriculture
- label: Energy
value: energy
- label: Water and wastewater
value: waterAndWastewater
- type: select
key: words
label: Pick some words
tags:
- autocomplete
multiple: true
customOptions:
maxItemCount: 3
properties:
en:autocomplete.maxItemText: "Sorry, {{count}} tags max!"
description: |
This uses <code>multiple</code> and
<code>customOptions.maxItemCount</code> to limit the number of
choices, and the <code>autocomplete.maxItemText</code> translation
key to change the text that shows up when two are selected.
data:
values:
- label: green
value: green
- label: empathic
value: empathic
- label: never
value: never
- label: eleven
value: eleven
- label: dry
value: dry
- label: valuable
value: valuable
- label: fortify
value: fortify
- label: whatever
value: whatever
- label: leaven
value: leaven
- type: htmlelement
tag: div
content: |
<b>ProTip:</b> Add <a
href="?language=debug"><code>?language=debug</code></a> to the
query string to see the <code>autocomplete.*</code> translation keys
in context.
options:
i18n:
debug:
autocomplete.noResultsText: 'autocomplete.noResultsText'
autocomplete.itemSelectText: 'autocomplete.itemSelectText'
autocomplete.maxItemText: 'autocomplete.maxItemText'
autocomplete.noChoicesText: 'autocomplete.noChoicesText'
autocomplete.searchPlaceholderValue: 'autocomplete.searchPlaceholderValue'

- id: radio
title: Radio
form:
Expand Down
11 changes: 9 additions & 2 deletions src/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,5 +108,12 @@
"words": "words",
"year": "Year",
"Year": "Year",
"Yes, delete it": "Yes, delete it"
}
"Yes, delete it": "Yes, delete it",
"autocomplete": {
"noResultsText": "No results found",
"itemSelectText": "",
"maxItemText": "Only {{count}} values can be added",
"noChoicesText": "No choices to choose from",
"searchPlaceholderValue": "Type to search"
}
}
41 changes: 36 additions & 5 deletions src/patch.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ function patch (Formio) {

loadEmbeddedTranslations(model, form.i18next)

patchSelectMode(model)
patchSelectMode(model, form)
form.form = model

for (const [event, handler] of Object.entries(eventHandlers)) {
Expand Down Expand Up @@ -226,17 +226,48 @@ function patch (Formio) {
})
}

function patchSelectMode (model) {
function patchSelectMode (model, form) {
const selects = FormioUtils.searchComponents(model.components, { type: 'select' })
for (const component of selects) {

// forEach() instead of for...of gives us a closure,
// which is important because the component reference needs to
// persist for functions like searchPlaceholderValue()
selects.forEach(component => {
const compKey = component.key
if (component.tags && component.tags.includes('autocomplete')) {
const t = (prop, ...rest) => {
const key = `autocomplete.${prop}`
const fallback = dot.get(defaultTranslations.en, key) || ''
return form.t([
`${compKey}.${key}`,
key,
fallback
], ...rest)
}

component.customOptions = Object.assign({
shouldSort: true
// shown when no results match the search input
noResultsText: t('noResultsText'),
// shown when no options are available (or loaded from an API)
noChoicesText: t('noChoicesText'),
// this overrides addItemText if provided
itemSelectText: t('itemSelectText'),
searchPlaceholderValue: t('searchPlaceholderValue'),
addItemText: component.customOptions?.addItemText ? value => {
return t('addItemText', {
value: FormioUtils.sanitize(value, {
sanitizeConfig: component.customOptions?.sanitize
})
})
} : false,
maxItemText (count) {
return t('maxItemText', { count })
}
}, component.customOptions)
} else {
component.widget = 'html5'
}
}
})
}

function patchLanguageObserver () {
Expand Down
13 changes: 13 additions & 0 deletions views/docs.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,19 @@
margin-top: 20px;
}

li + li {
margin-top: 5px;
}

table {
margin: 20px 0;
}

th,
td {
padding: 5px;
}

a[aria-hidden="true"] {
position: absolute;
right: 100%;
Expand Down

1 comment on commit e0dc789

@vercel
Copy link

@vercel vercel bot commented on e0dc789 Feb 2, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.