From f6373b1b053a95df96c04388ebcd5dfe483aeb0b Mon Sep 17 00:00:00 2001 From: Sarah Higley Date: Wed, 4 Aug 2021 08:49:03 -0700 Subject: [PATCH 1/3] add rfc for i18n --- rfcs/convergence/internationalization.md | 168 +++++++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 rfcs/convergence/internationalization.md diff --git a/rfcs/convergence/internationalization.md b/rfcs/convergence/internationalization.md new file mode 100644 index 0000000000000..da9cb20aaae0f --- /dev/null +++ b/rfcs/convergence/internationalization.md @@ -0,0 +1,168 @@ +# RFC: Internationalization patterns + +--- + +_List contributors to the proposal:_ @smhigley + +## Problem Statement: + +In v8, the custom strings authors must define to use components are all over the place, and have no standard pattern. E.g. Datepicker uses a `strings` prop and imports defaults from an in-package module + +Most other components define each string as a separate property, with no standard naming convention. For example: + +- ContextualMenu has `ariaLabel` and `ariaDescription` on menuitems +- BasePicker uses `aria-label` +- Button has `ariaLabel`, `ariaDescription`, `text`, `splitButtonAriaLabel`, and `secondaryText` +- icons use `ariaLabel` for a string that isn't necessarily defined through `aria-label`. + +While not universally true, a lot of these strings are specifically for screen readers, which means there's a higher likelihood developers will fail to notice, define, and localize them if they're not clearly surfaced. Even when that isn't the case, it's a pain to handle localization for all components across an app when each takes strings through a different API surface. + +## Background + +I looked at how other component libraries do this, and both [Material UI](https://material-ui.com/guides/localization/) and [Ant Design](https://ant.design/docs/react/i18n) take a similar approach: + +They both have a utility to provide or define custom locale objects that include all the strings for all applicable components. For example, this is a sample of how the [exported spanish locale object for MUI](https://unpkg.com/browse/antd@4.16.9/lib/locale/es_ES.js) is defined: + +```js +var localeValues = { + locale: 'es', + global: { + placeholder: 'Seleccione' + }, + Table: { + filterTitle: 'Filtrar menĂº', + emptyText: 'Sin datos', + selectAll: 'Seleccionar todo', + [...] + }, + Modal: { + okText: 'Aceptar', + cancelText: 'Cancelar', + justOkText: 'Aceptar' + } + [...etc] +} +``` + +Material UI includes the locale strings in their ThemeProvider, and Ant Design puts it in their ConfigProvider. + +Libraries where I found no tools or defined patterns for localization include Lightning Design System, Semantic UI, React Bootstrap, and Carbon Design System. + +Two of the most common i18n packages for React independent of component libraries are `react-i18next` (a react wrapper for i18next), and `react-intl`. Both provide a helper function to format a string -- at the most basic, they take a key and return a localized string. The most basic usage looks like this for each library: + +react-i18next: + +```js +import { useTranslation } from 'react-i18next'; + +const Component = () => { + const { t } = useTranslation(); + + return
{t('key')}
; +}; +``` + +react-intl: + +```js +import { useIntl } from 'react-intl'; + +const Component = () => { + const intl = useIntl(); + + return
{intl.formatMessage({ id: 'key' })}
; +}; +``` + +Both also provide extras like React components as an alternative to the function, and helpers for things like dates, string template value replacement, SSR, etc. Those likely aren't relevant to Fluent, since we'll either not use them in the case of the component or SSR, or provide our own solution for dates/string templates. + +## Proposal + +While we don't have plans to provide actual translations for the strings in any of our components, it'd be nice to create a pattern for defining custom strings that makes it easy for authors to: + +- Easily propagate app-level locale strings down to Fluent components without needing to manually map them to multiple separate component props +- Easily include all necessary component strings when they create a new locale definition/JSON file (i.e. not need to manually remember that TagPicker needs `selectionAriaLabel` defined). +- Use 3rd party i18n tools with Fluent components without too much extra work + +To do this, I think it makes sense to add locale data to Fluent's `ProviderContext`, since it already includes `dir`. + +One straightforward option for how to do this would be to provide a locale object with static key/value pairs for each string. We could then import defaults for all strings where defaults make sense, which would allow us to also share global strings for things like 'close'/'remove'/'select all'/etc. So something like this (specific file names and values are just for show): + +react-shared-contexts/src/ProviderContext/defaultStrings.ts + +```js +export const defaultLocale { + locale: 'en-US', + strings: { + global: { + close: 'close', + remove: 'remove' + }, + Button: { + splitButtonLabel: 'more options' + } + // etc + } +} +``` + +react-shared-contexts/src/ProviderContext/ProviderContext.ts + +```js +import { defaultLocale } from './defaultStrings.ts'; + +export const ProviderContext = + React.createContext < + ProviderContextValue > + { + targetDocument: typeof document === 'object' ? document : undefined, + dir: 'ltr', + locale: defaultLocale, + }; +``` + +Then, each component would take a `strings` prop where strings could be directly passed in, but would default to the provided locale, if available: + +react-dropdown/src/components/useDropdown.tsx (very simplified) + +```js +export const useDropdown = props => { + const { locale } = useFluent(); + const dropdownStrings = { + ...locale.strings.global, // only if this component actually uses global strings + ...locale.strings.Dropdown, + ...props.strings, + }; + + state.components.arrow['aria-label'] = dropdownStrings.open; +}; +``` + +A second possibility is that we could also provide a way to (optionally) pass a function (e.g. `localizeString`) through `ProviderContext` to better support libraries like `react-i18next` and `react-intl`. Then, if that function is provided, we call it with any given string as an argument. That would mean someone using `react-i18next` could do something like this: + +App.ts: + +```js +import { useTranslation } from 'react-i18next'; + +const App = () => { + const { t, i18n } = useTranslation('es-ES'); + + const localeKeys = { + /* define strings as keys to be passed in to localizeString */ + }; + + return ( + + + + ); +}; +``` + +## Things to consider + +- How will this affect SSR? +- Could we create a script to generate a blank locale JSON file as a template for an author-selected set of components? +- Flexibility/integration with other 3rd party localization tools or patterns +- Bundle size if all components' strings are included in defaultStrings (at least in the English/default set of strings) From 06e2bed51bf4f962d4c383a65bf65110db07ecc9 Mon Sep 17 00:00:00 2001 From: Sarah Higley Date: Tue, 21 Sep 2021 14:30:02 -0700 Subject: [PATCH 2/3] updated RFC with specific global strings, and vNext beta rec --- rfcs/convergence/internationalization.md | 69 ++++++++++++++++++++---- 1 file changed, 59 insertions(+), 10 deletions(-) diff --git a/rfcs/convergence/internationalization.md b/rfcs/convergence/internationalization.md index da9cb20aaae0f..3937da98f8c33 100644 --- a/rfcs/convergence/internationalization.md +++ b/rfcs/convergence/internationalization.md @@ -86,7 +86,7 @@ While we don't have plans to provide actual translations for the strings in any To do this, I think it makes sense to add locale data to Fluent's `ProviderContext`, since it already includes `dir`. -One straightforward option for how to do this would be to provide a locale object with static key/value pairs for each string. We could then import defaults for all strings where defaults make sense, which would allow us to also share global strings for things like 'close'/'remove'/'select all'/etc. So something like this (specific file names and values are just for show): +One straightforward option for how to do this would be to provide a locale object with static key/value pairs for each string. We could provide a minimal set of global strings in the top-level provider, similar to how Material and Ant differentiate global vs. component strings. Based on current Fluent v8 and N\* strings, it would look like this: react-shared-contexts/src/ProviderContext/defaultStrings.ts @@ -95,13 +95,13 @@ export const defaultLocale { locale: 'en-US', strings: { global: { + more: 'more items', close: 'close', - remove: 'remove' - }, - Button: { - splitButtonLabel: 'more options' + noResults: 'no results found', + loading: 'loading', + selectAll: 'select all', + expandCollapse: 'expand {0}' } - // etc } } ``` @@ -121,11 +121,21 @@ export const ProviderContext = }; ``` -Then, each component would take a `strings` prop where strings could be directly passed in, but would default to the provided locale, if available: +Then, each component would take a `strings` prop where strings could be directly passed in, and also provide component-specific defaults when needed. The `strings` prop would also be used for the author to pass in strings that don't have a default, e.g. error messaging and instance-specific labels. + +react-dropdown/src/localeStrings.ts (specific strings are just for this example) + +```js +export const dropdownDefaultStrings: Partial { + loadMore: 'Load more options', + invalidEntryError: 'Your search did not match any options' +} +``` react-dropdown/src/components/useDropdown.tsx (very simplified) ```js +import { dropdownDefaultStrings } from '../localeStrings'; export const useDropdown = props => { const { locale } = useFluent(); const dropdownStrings = { @@ -134,11 +144,51 @@ export const useDropdown = props => { ...props.strings, }; - state.components.arrow['aria-label'] = dropdownStrings.open; + state.components.arrow['aria-label'] = dropdownStrings.expandCollapse; +}; +``` + +## Concrete actions for vNext beta + +The only action needed for beta release should be to ensure all strings provided through component props (excluding children/child content) are defined in a single `strings` property. + +For example, the `PresenceBadge` (and also components like `Avatar` that use it) would need something like this for the strings prop (likely with defaults in a component-specific file for `PresenceBadge`): + +```js +interface PresenceBadgeStrings { + statusBusy: string; + statusOutOfOffice: string; + statusAway: string; + statusAvailable: string; + statusOffline: string; + statusDnD: string; +} + +interface PresenceBadgeProps { + // ...etc + + strings?: PresenceBadgeStrings; +} + +interface AvatarStrings extends PresenceBadgeStrings { + active: string; + inactive: string; +} + +// defined in a default strings file +const presenceDefaultStrings: PresenceBadgeStrings = { + statusBusy: 'busy', + statusOutOfOffice: 'out of office', + statusAway: 'away', + statusAvailable: 'available', + statusOffline: 'offline', + statusDnD: 'do not disturb', }; ``` -A second possibility is that we could also provide a way to (optionally) pass a function (e.g. `localizeString`) through `ProviderContext` to better support libraries like `react-i18next` and `react-intl`. Then, if that function is provided, we call it with any given string as an argument. That would mean someone using `react-i18next` could do something like this: +## Future possibilities + +Another possibility is that we could also provide a way to (optionally) pass a function (e.g. `localizeString`) through `ProviderContext` to better support libraries like `react-i18next` and `react-intl`. Then, if that function is provided, we call it with any given string as an argument. That would mean someone using `react-i18next` could do something like this: App.ts: @@ -165,4 +215,3 @@ const App = () => { - How will this affect SSR? - Could we create a script to generate a blank locale JSON file as a template for an author-selected set of components? - Flexibility/integration with other 3rd party localization tools or patterns -- Bundle size if all components' strings are included in defaultStrings (at least in the English/default set of strings) From 0033350156c6ffaf8dfef89e1e7e36ea82758a62 Mon Sep 17 00:00:00 2001 From: Sarah Higley Date: Wed, 11 May 2022 01:38:42 -0700 Subject: [PATCH 3/3] update rfc with multi-step approach --- rfcs/convergence/internationalization.md | 124 ++++++++++++++++------- 1 file changed, 87 insertions(+), 37 deletions(-) diff --git a/rfcs/convergence/internationalization.md b/rfcs/convergence/internationalization.md index 3937da98f8c33..a0463cec6a82d 100644 --- a/rfcs/convergence/internationalization.md +++ b/rfcs/convergence/internationalization.md @@ -19,6 +19,8 @@ While not universally true, a lot of these strings are specifically for screen r ## Background +### Other component libraries + I looked at how other component libraries do this, and both [Material UI](https://material-ui.com/guides/localization/) and [Ant Design](https://ant.design/docs/react/i18n) take a similar approach: They both have a utility to provide or define custom locale objects that include all the strings for all applicable components. For example, this is a sample of how the [exported spanish locale object for MUI](https://unpkg.com/browse/antd@4.16.9/lib/locale/es_ES.js) is defined: @@ -46,7 +48,37 @@ var localeValues = { Material UI includes the locale strings in their ThemeProvider, and Ant Design puts it in their ConfigProvider. -Libraries where I found no tools or defined patterns for localization include Lightning Design System, Semantic UI, React Bootstrap, and Carbon Design System. +Among other libraries, there are a variety of approaches to built-in string values that range from hardcoded values to per-string props: + +#### Libraries with no localization approach: + +- **Semantic UI**: didn't find built-in strings, in multiple places accnames were missing (e.g. +/- on Dimmer) +- **FAST**: did not find built-in strings, in multiple places accnames were missing (e.g. the flipper button) +- **Evergreen**: Hardcoded English strings, or missing labels (e.g. browse/drag copy in FileUploader, missing prev/next labels in Pagination) +- **React Bootstrap**: English strings hardcoded (e.g. "Next", "Last" in Pagination) + +#### Libraries with only per-string props: + +- **Spectrum**: individual props (e.g. "labelX", "labelY" on ColorArea) +- **Carbon**: a `locale` prop on datepicker, `translationIds` + `translationKeys` on other components, misc props on simpler components (e.g. `backwardText` on pagination) +- **Atlassian**: misc props (e.g. `nextLabel`, `previousLabel` on Pagination) + +#### Libraries with only a provider (no props): + +- **Ant** (sort of): some Ant components like Upload or Table only accept localized strings through a LocaleReceiver + ConfigProvider + +#### Libraries with a combination of props and a provider: + +- **Ant**: Date/time components have `locale` prop that includes strings in addition to the LocaleReceiver + ConfigProvider approach. +- **Material UI**: misc props on components + locale support on the theme provider (e.g. `clearText`, `closeText` on Autocomplete) + +### Learnings from Fluent v8 + +The Fluent v8 approach has largely been piecemeal, with per-component props added for internal strings. There is no standard naming schema, and for authors to implement proper a11y and localization, they've needed to hunt down the string props for each component. + +We've had feedback, largely related to accessibility bugs, that this approach is onerous and frustrating to authors. There has been some frustration expressed specifically around the experience of feeling like there is a "gotcha" nature to accessibility where our components technically support accessible labels, but authors need to really dig to find out how to implement them. + +### i18n libraries Two of the most common i18n packages for React independent of component libraries are `react-i18next` (a react wrapper for i18next), and `react-intl`. Both provide a helper function to format a string -- at the most basic, they take a key and return a localized string. The most basic usage looks like this for each library: @@ -78,17 +110,59 @@ Both also provide extras like React components as an alternative to the function ## Proposal -While we don't have plans to provide actual translations for the strings in any of our components, it'd be nice to create a pattern for defining custom strings that makes it easy for authors to: +We can split work on this into three phases: + +### 1. A prop for overriding internal strings + +Most other libraries that have i18n solutions include some sort of props-based approach. Based on the feedback received from Fluent v8, it seems best to make this a standard prop across all components that have internal strings. + +The specific name for this prop could be `strings` (which matches the Fluent v8 Datepicker), `locale` (which matches a couple other libraries), or something else entirely. + +The benefits to providing a prop on individual components vs. a provider-only solution include: + +- It is easier to use a few Fluent controls in isolation, e.g. within an app that also uses controls from another library +- It provides greater control and flexibility. For example, if a team were using a third-party solution with more complex logic than a built-in Fluent string provider, they could always override strings at a component level. +- We can ship components with internal strings now, and then integrate a string provider in the future + +The benefits of a single `strings`/`locale`/`someSortOfLabel` prop over requiring authors to manually figure out `aria-*`/`