From 94f984ba2175b4ce8599af622d43af233a0f1641 Mon Sep 17 00:00:00 2001 From: Malcolm Smith <20709258+msmithNI@users.noreply.github.com> Date: Thu, 25 May 2023 20:24:49 -0500 Subject: [PATCH 01/12] HLD and doc updates --- .../nimble-components/docs/accessibility.md | 14 +- specs/labels-and-localization/README.md | 188 ++++++++++++++++++ 2 files changed, 200 insertions(+), 2 deletions(-) create mode 100644 specs/labels-and-localization/README.md diff --git a/packages/nimble-components/docs/accessibility.md b/packages/nimble-components/docs/accessibility.md index c3d3d17517..6875647ab4 100644 --- a/packages/nimble-components/docs/accessibility.md +++ b/packages/nimble-components/docs/accessibility.md @@ -26,5 +26,15 @@ When setting [ARIA attributes](https://developer.mozilla.org/en-US/docs/Web/Acce # Component-specific accessiblity notes -## nimble-button -When using the `nimble-button` with only an icon, `contentHidden` should be set to `true` and text content should be provided within the button even though it will not be visible on the screen. The button will use the text content to configure the appropriate ARIA attributes internally to ensure the button is adequately accessible with a screen reader. \ No newline at end of file +## Buttons +`nimble-button`, `nimble-anchor-button`, `nimble-menu-button`, `nimble-toggle-button` + +When using the Nimble buttons with only an icon, `contentHidden` should be set to `true` and text content should be provided within the button even though it will not be visible on the screen. The button will use the text content to configure the appropriate ARIA attributes internally to ensure the button is adequately accessible with a screen reader. + +## nimble-banner +When using `nimble-banner`, the title content should always be provided for accessibility. To hide the title visually, `titleHidden` can be set to `true`. + +## Icons +When using an icon standalone, the `title` attribute should usually be set, to provide accessible text (which will also show as a tooltip). + +When using an icon as the content of a Nimble button, place the icon in the `start` slot, and follow the button guidance above. diff --git a/specs/labels-and-localization/README.md b/specs/labels-and-localization/README.md new file mode 100644 index 0000000000..c454677445 --- /dev/null +++ b/specs/labels-and-localization/README.md @@ -0,0 +1,188 @@ +# Localization for Labels in Nimble Components + +## Problem Statement + +We want to have consistent guidance for when & how to use labels for accessible text inside Nimble components. We also need a plan for how to support localization to non-English languages for those labels. + +## Links To Relevant Work Items and Reference Material + +[#1090: Unified strategy for providing localized title and aria label for icons within components](https://github.com/ni/nimble/issues/1090) + +[PR 1257: Runtime Configuration HLD](https://github.com/ni/nimble/pull/1257) + +## Accessible Labels +For many Nimble components, we already have a sufficient strategy for accessible labels. Guidance for specific components is documented in [nimble-components/docs/accessibility.md](../../packages/nimble-components/docs/accessibility.md). + +Issue #1090 primarily covers the various icons used in Nimble components. A summary of our plan for accessible labels for icons currently used in Nimble components is: +- Increment/decrement buttons in `nimble-number-field` ([#617](https://github.com/ni/nimble/issues/617)): These are `nimble-button` with `content-hidden`, and "Increment" and "Decrement" text. That approach is sufficient, but the text needs localization. +- `nimble-banner` and `nimble-tooltip` severity icons: No accessible label specifically for the severity icons is currently planned. The banner/tooltip text should usually be sufficient to indicate if the message is an error or informational, so additional "Warning" or "Error" labels would usually be redundant. For the banner, the accessible text comes from the `title`. If we need to specifically support high-severity errors that we want to call the user's attention to, we could consider using the ARIA `role=alert` in that case. +- `nimble-banner` dismiss button icon: `nimble-button` with `content-hidden`, text comes from `dismissButtonLabel`, with a fallback as "Close". That approach is sufficient. `dismissButtonLabel` should be localized by the clients, the "Close" fallback text is Nimble-provided and needs localization. + +The Nimble table will have many labels, which are summarized here: +- Expand/Collapse Group button, Collapse All buttons: These are already `nimble-button` with `content-hidden`, but currently contain no text. We need to add text content for them (which will need localization). +- Action menu button in table cells ([#859](https://github.com/ni/nimble/issues/859)): Base table column already has `action-menu-label` for the accessible label (for which clients would handle localizing themselves). Currently providing `action-menu-label` is not required, so we may want to define a table fallback string to use if it's not provided (e.g. "Configure" or "Options") which would need localization. +- Column menu (button in column header): Will need a Nimble-provided label (like "Column Options") which will need localization. Each menu item will be both an icon and a visible label/ menu text, so no `title`/label specifically on the icons in the menu items is needed. Each menu item's text will be Nimble-provided (and need localization). +- Sort indicators (ascending/descending icon) in table headers: No accessible label specifically for these icons is currently planned. The primary sorted column is already indicated with `aria-sort="ascending"` or `aria-sort="descending"`. The [ARIA APG's sortable table example](https://www.w3.org/WAI/ARIA/apg/patterns/table/examples/sortable-table/) follows similar logic, and says sort labels are not added to each column header button "to prevent repetitious verbosity that could interfere with understanding of the column titles". +- Client-provided icon elements in the table (e.g. icons as the primary content of table headers): Follows the same guidance in [accessibility.md](../../packages/nimble-components/docs/accessibility.md), i.e. the icons should provide accessible text via the `title` attribute (and clients will handle localizing it themselves). +- (Mapping/ Icon Columns: As currently specced, `nimble-table-column-icon`'s `nimble-mapping-icon` has a `label` attribute which will become the icon `title`) + +## Localization + +Our general approach to localization is that clients will handle localizing strings. That has led us to add attributes on some Nimble components for clients to provide localized labels, but that doesn't scale well to components with many labels like the table. + +### Plan for Nimble-Provided Labels + +Conceptually the localized strings are very similar to the sharing pattern of Design Tokens. The vast majority of the time, you want to use the same value (i.e. numeric increment/decrement button text or collapse button text is not control instance specific) but want to adjust to a global config, i.e. theme or language. But also want to be able to override for specific controls as needed. + +We will create non-CSS-property design tokens, similar to the `theme` and `direction` design tokens. The values can be set via the `nimble-theme-provider`: + +```html + + + +``` + +Pros: + +- Easy discovery of localized string configuration. They are all on the theme-provider. +- Can configure once for the whole page in one spot, does not need to be per control. Much fewer duplicated attributes / slotted elements in the page. +- Can override for specific elements as needed by wrapping in a theme-provider. +- Can configure to be attribute or slotted on the theme provider. But if don't need mixed content then attribute makes sense. +- User provided control-specific content should still be provided via attributes / slots. This is for control provided content (the table control defines the collapse button, not the user). + +Cons: + +- Still somewhat verbose in the page, to have all of the localized strings as attributes + - We can consider using the `fromView` attribute mode on the attributes for the strings on the theme-provider. If clients set the strings using the theme-provider properties rather than as attributes in the HTML (as is currently proposed for Angular), `fromView` would not reflect those strings back to the DOM. +- Not an originally intended use case for DesignToken +- Only nimble-components defined within Nimble can add new attributes to the theme-provider. + - We're encouraging clients to add new components to Nimble, including custom column types for the table, and those can use this approach + - Components outside Nimble could still have accessible labels by defining attributes on their components for them + +#### Implementation Details + +See the prototype branch: [localizable-labels-prototype](https://github.com/ni/nimble/commit/0b088a67af4a860fce17003e37ea1bf8dfd10e8b) + +**nimble-components** +We'll create a file containing the label IDs and English string values. A simple version of this is a JSON file (Prototype: [`labels.json`](https://github.com/ni/nimble/commit/0b088a67af4a860fce17003e37ea1bf8dfd10e8b#diff-0e77bd18c80fb68ad9624a1eeccbec4a539f98b92e2ff97a013f763cc897a8ce)): +```json +{ + "labelNumberFieldIncrement": "Increment", + "labelNumberFieldDecrement": "Decrement", + ... +} +``` +We'll code-gen source files from that (Prototype: [`build/generate-labels/`](https://github.com/ni/nimble/commit/0b088a67af4a860fce17003e37ea1bf8dfd10e8b#diff-7eb7cdf375d9cf88a5e1083a264b299d3143779ce57906cc42aaa408df354f1a)): +- For each label, an exported `DesignToken` for it (with default of the English string). Prototype: [`labels/index.ts`](https://github.com/ni/nimble/commit/0b088a67af4a860fce17003e37ea1bf8dfd10e8b#diff-67d489e618f4180c3d10019eb8252b7b13a5f52516585c6baba2e4fc6d5b9914) +- Collections of the label names and values, which can be used in subsequent builds / Storybook stories. Prototype: `label-names.ts` and `label-values.ts` +- A `ThemeProviderBase` class with `@attr` properties and `Changed()` methods for each label, which ThemeProvider will derive from. Prototype: [`theme-provider-base.ts`](https://github.com/ni/nimble/commit/0b088a67af4a860fce17003e37ea1bf8dfd10e8b#diff-e90f67f575dfcd6fd5835a58a2663c8d1f05c0837328902751c8759fac817df9). (This may change slightly with the proposed `nimble-global-theme-provider` from PR 1257. The expectation is that both ThemeProvider and the GlobalThemeProvider would have the same attributes/properties to configure the labels.) + +**nimble-angular** +One goal for the nimble-angular label support is to make it easy (or automatic) for clients to pick up new localized strings/labels when they uptake new nimble-angular versions. The way the prototype accomplishes that is: +- Export (codegen) a [`NimbleLabels` class](https://github.com/ni/nimble/commit/0b088a67af4a860fce17003e37ea1bf8dfd10e8b#diff-8ada6f05dc0978bee3e574b5e06167631a98b8fd1ac8f5467aa3828e3ba933f1) that has properties for all Nimble labels, and uses the Angular `localize` function: +```ts +export class NimbleLabels { + public static readonly labelNumberFieldIncrement = $localize`Increment`; + public static readonly labelNumberFieldDecrement = $localize`Decrement`; +} +``` +(We may encode the label ID as the description so it shows up in the message files, i.e. ``$localize`:nimble-label-number-field-decrement:Decrement` ``). +If a consumer of nimble-angular imports the module containing the `NimbleLabels` class, once they run `ng extract-i18n` for their app, the message files will then contain the Nimble strings for translation. Additionally, once a client has done that, labels/strings in future nimble-angular version updates would automatically get pulled in by `ng extract-i18n` if it was run again after a package update. +- Export (codegen) a directive which will automatically set all of the labels on a Nimble theme provider, with the values from `NimbleLabels`. In the prototype this is [`NimbleThemeProviderInitializeAllLabelsDirective`](https://github.com/ni/nimble/commit/0b088a67af4a860fce17003e37ea1bf8dfd10e8b#diff-a8bb0152269fed064bdc90ef28541bdedaeaf86f3a4e2b1751d2da68a6090aca) with selector `nimble-theme-provider[initializeAllLabels]`. Assuming that an app is OK with pulling in and localizing all of Nimble's strings/labels, using this directive will make the process of using those localized strings mostly automatic. +- Codegen [`NimbleThemeProviderBaseDirective`](https://github.com/ni/nimble/commit/0b088a67af4a860fce17003e37ea1bf8dfd10e8b#diff-a339dcfbd672a173f9daf1b67e96ed59cd7de2167935aae338d76a277476b026): Similar to the nimble-components version, this exposes properties for all of the Nimble labels. This allows apps to pick & choose to only localize a few labels (e.g. if they're only using a subset of the controls), or override specific label values (set on an ancestor theme provider, or override the values from the *InitializeAllLabels directive). However the app wouldn't automatically pick up new strings if they only set labels this way. + +*Possible Alternatives:* +- Codegen an Angular component that contains a `nimble-theme-provider`, sets the label attributes in the component HTML, adds `i18n-` versions of the attributes, and contains ``, then apps would use this component instead of nimble-theme-provider. Not prototyped so not sure if this is feasible or not. +- Same as the previous idea, but the component is manually created in systemlink-lib-angular. This should work fine, but since it's not in Nimble / not codegen'd from the strings, it would be a manual process to update it with new Nimble labels in new nimble-angular versions. + +**nimble-blazor** +Label support for Nimble Blazor is more rudimentary than nimble-angular. Additionally, the prototype doesn't yet codegen for Blazor, but it has some hardcoded changes to illustrate the concepts: +- [`NimbleBlazor\NimbleResources\NimbleLabels.resx`](https://github.com/ni/nimble/commit/0b088a67af4a860fce17003e37ea1bf8dfd10e8b#diff-cfca56c824b9b81654e41d45429aa8dad759211ca1ceb8e5bac7886b75bbeab4): Contains labels and English values, i.e. `labelNumberFieldIncrement` => `'Increment'` (this would be codegen'd) + - In the [csproj](https://github.com/ni/nimble/commit/0b088a67af4a860fce17003e37ea1bf8dfd10e8b#diff-3635dce04cb0a44d829f97259dce31b8f2abf8d0e80a4c5ccf9506babc93a78aR38), the Build Action for this is Content (not EmbeddedResource), `copyToOutput=true`, `IncludeInPackage=true`. This results in the resx file being included in the NuGet, in [contentFiles/](https://learn.microsoft.com/en-us/nuget/reference/nuspec#using-the-contentfiles-element-for-content-files) +- [`Components\NimbleThemeProvider.razor`](https://github.com/ni/nimble/commit/0b088a67af4a860fce17003e37ea1bf8dfd10e8b#diff-935610f630ad061236774e447badf29f08dd1467f6c4c6e45ead7214641afe6d): Add code/markup for labels: i.e. `label-number-field-increment="@LabelNumberFieldIncrement"` in the Razor file, `[Parameter] public string? LabelNumberFieldIncrement { get; set; }` in the C# code. We may need to fully codegen the Razor file to add the attribute bindings. + +Once clients add a reference to the NuGet, `NimbleResources\NimbleLabels.resx` will appear in their project as a link (to that file in the NuGet). At that point, the remaining process is somewhat convoluted, and would need documentation. The prototype branch has done some of this in `Demo.Shared` and `Demo.Server`: +- Browse to that resx file on disk, and manually copy it into the app project directory, in `Resources\` +- Add it to the Blazor project (as Build Action = Embedded Resource). Then, open it in the resx editor, and set Access Modifier = Public or Internal (to generate the `.designer.cs` file) +- To translate/localize the resources, clone the resx file (e.g. `NimbleLabels.fr.resx`) and translate the values in it. The app also needs to follow the [Microsoft Blazor localization docs](https://learn.microsoft.com/en-us/aspnet/core/blazor/globalization-localization?view=aspnetcore-7.0&pivots=server) in terms of opting into localization, configuring the supported locales, etc. +- To consume the resources, in the Razor file using the NimbleThemeProvider, add: +```cs +[Inject] +internal IStringLocalizer? LabelProvider { get; set; } +``` +Then bind to the theme provider labels in the Razor template: +```xml + +``` + +The process above isn't great. When updated versions of Nimble Blazor are released, if they have new labels, the consuming app would need to repeat the process of manually copying the resx, and translating it. They'd also need to manually add the new label properties in their Razor file using the NimbleThemeProvider. + +If we have any clients that will be using Nimble Blazor and non-English locales we should probably do additional research to see if we can come up with a more seamless approach. + +*Possible Alternatives:* +- Add an `IStringLocalizer` property to the `NimbleThemeProvider` Razor component, and codegen the `Label*` properties to use it if it's set. This would improve the process of telling Nimble about the localized resources, but wouldn't change the process of manually copying the resx to start with. +- Define an MSBuild task that copies the resx file, similar to what's outlined in [this GitHub comment](https://github.com/ni/nimble/issues/558#issuecomment-1129279985). Not much better / still a manual process. + +### Plan for Client-Provided Labels +Examples: Button content, menu item content, `nimble-banner` `dismissButtonLabel`, `nimble-table` column `action-menu-label` + +Almost all of these labels are context-specific, i.e. different buttons on the page will have different text, and different columns will have different action menu labels. + +Clients will localize those labels themselves: +- For Angular, they can use the `localize` function and/or `i18n` attributes/ attribute prefixes +- For Blazor, they can use `.resx` files and `IStringLocalizer` + +The banner's `dismissButtonLabel` will be semi-redundant once we have a Nimble-provided label for banner dismiss buttons, so we could consider removing that attribute in the future. + +## Alternative Implementations / Designs + +Alternative options for Nimble-provided labels: +### Slots + +Create slots per localized string on the element. + +```html + + Hello World + +``` + +Pros: +- Useful for mixed content (if that is even needed, maybe not?) +- Can maybe make shareable slotted content within a framework + - Angular: Create a shared component that emits elements with those slots + - Blazor: I expect a blazor component written that way would work too +- Slots can have default content easily + +Cons: +- DOM gets littered with many additional elements for providing localized strings +- Lots of duplicated slotted content / elements. Have to emit into every table element as children. + +### Attributes + +Create attribute per localized string on the element. + +```html + +``` + +Pros: +- Easy discovery of available properties? Type checked in Angular at least + +Cons: +- Maybe not as easy to share / reuse attribute configuration? + - Angular: Might need to write a custom directive that will emit the attributes onto the host. I think that works + - Blazor: ? +- Lots of duplicated attributes. Have to emit into every table element as children. +- Only text content (maybe that's okay) + +## Open Issues + +- Are the label IDs (`label-number-field-increment`) sufficient information for someone to translate the strings from? (Should we include an additional/optional description with each label/string?) +- Does Nimble need to support more advanced use cases of strings/labels like format specifiers or [ICU expressions](https://angular.io/guide/i18n-common-prepare#icu-expressions)? From b8dbd6f4f8e0a4004e821006c0a011ef44c783f1 Mon Sep 17 00:00:00 2001 From: Malcolm Smith <20709258+msmithNI@users.noreply.github.com> Date: Thu, 25 May 2023 20:42:07 -0500 Subject: [PATCH 02/12] Change files --- ...le-components-2dda6a9d-d30f-45f2-b086-f5564c763990.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 change/@ni-nimble-components-2dda6a9d-d30f-45f2-b086-f5564c763990.json diff --git a/change/@ni-nimble-components-2dda6a9d-d30f-45f2-b086-f5564c763990.json b/change/@ni-nimble-components-2dda6a9d-d30f-45f2-b086-f5564c763990.json new file mode 100644 index 0000000000..ae1665c77e --- /dev/null +++ b/change/@ni-nimble-components-2dda6a9d-d30f-45f2-b086-f5564c763990.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "Update docs (accessible labels for button/banner/icons)", + "packageName": "@ni/nimble-components", + "email": "20709258+msmithNI@users.noreply.github.com", + "dependentChangeType": "none" +} From 3fa4044a359d144d97e9353ee8b65ac1304db357 Mon Sep 17 00:00:00 2001 From: Malcolm Smith <20709258+msmithNI@users.noreply.github.com> Date: Tue, 30 May 2023 16:30:12 -0500 Subject: [PATCH 03/12] HLD feedback --- packages/nimble-components/docs/accessibility.md | 3 +++ specs/labels-and-localization/README.md | 6 ++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/nimble-components/docs/accessibility.md b/packages/nimble-components/docs/accessibility.md index 6875647ab4..89ede647c7 100644 --- a/packages/nimble-components/docs/accessibility.md +++ b/packages/nimble-components/docs/accessibility.md @@ -34,6 +34,9 @@ When using the Nimble buttons with only an icon, `contentHidden` should be set t ## nimble-banner When using `nimble-banner`, the title content should always be provided for accessibility. To hide the title visually, `titleHidden` can be set to `true`. +## nimble-dialog +When using `nimble-dialog`, the title content should always be provided for accessibility. To hide the title visually, `headerHidden` can be set to `true`. + ## Icons When using an icon standalone, the `title` attribute should usually be set, to provide accessible text (which will also show as a tooltip). diff --git a/specs/labels-and-localization/README.md b/specs/labels-and-localization/README.md index c454677445..19ca508a90 100644 --- a/specs/labels-and-localization/README.md +++ b/specs/labels-and-localization/README.md @@ -22,7 +22,8 @@ The Nimble table will have many labels, which are summarized here: - Expand/Collapse Group button, Collapse All buttons: These are already `nimble-button` with `content-hidden`, but currently contain no text. We need to add text content for them (which will need localization). - Action menu button in table cells ([#859](https://github.com/ni/nimble/issues/859)): Base table column already has `action-menu-label` for the accessible label (for which clients would handle localizing themselves). Currently providing `action-menu-label` is not required, so we may want to define a table fallback string to use if it's not provided (e.g. "Configure" or "Options") which would need localization. - Column menu (button in column header): Will need a Nimble-provided label (like "Column Options") which will need localization. Each menu item will be both an icon and a visible label/ menu text, so no `title`/label specifically on the icons in the menu items is needed. Each menu item's text will be Nimble-provided (and need localization). -- Sort indicators (ascending/descending icon) in table headers: No accessible label specifically for these icons is currently planned. The primary sorted column is already indicated with `aria-sort="ascending"` or `aria-sort="descending"`. The [ARIA APG's sortable table example](https://www.w3.org/WAI/ARIA/apg/patterns/table/examples/sortable-table/) follows similar logic, and says sort labels are not added to each column header button "to prevent repetitious verbosity that could interfere with understanding of the column titles". +- Sort indicators (ascending/descending icon) in table column headers: No accessible label specifically for these icons is currently planned. The primary sorted column is already indicated with `aria-sort="ascending"` or `aria-sort="descending"`. The [ARIA APG's sortable table example](https://www.w3.org/WAI/ARIA/apg/patterns/table/examples/sortable-table/) follows similar logic, and says sort labels are not added to each column header button "to prevent repetitious verbosity that could interfere with understanding of the column titles". +- Grouped indicator in table column headers: Currently no ARIA attributes indicate a grouped column, so we'll probably want to add a label for this (probably via a `title` / tooltip) - Client-provided icon elements in the table (e.g. icons as the primary content of table headers): Follows the same guidance in [accessibility.md](../../packages/nimble-components/docs/accessibility.md), i.e. the icons should provide accessible text via the `title` attribute (and clients will handle localizing it themselves). - (Mapping/ Icon Columns: As currently specced, `nimble-table-column-icon`'s `nimble-mapping-icon` has a `label` attribute which will become the icon `title`) @@ -57,7 +58,7 @@ Pros: Cons: - Still somewhat verbose in the page, to have all of the localized strings as attributes - - We can consider using the `fromView` attribute mode on the attributes for the strings on the theme-provider. If clients set the strings using the theme-provider properties rather than as attributes in the HTML (as is currently proposed for Angular), `fromView` would not reflect those strings back to the DOM. + - We can consider using the `fromView` attribute mode on the attributes for the strings on the theme-provider. If clients set the strings using the theme-provider properties rather than as attributes in the HTML (as is currently proposed for Angular), `fromView` would not reflect those strings back to the DOM. - Not an originally intended use case for DesignToken - Only nimble-components defined within Nimble can add new attributes to the theme-provider. - We're encouraging clients to add new components to Nimble, including custom column types for the table, and those can use this approach @@ -96,6 +97,7 @@ If a consumer of nimble-angular imports the module containing the `NimbleLabels` - Codegen [`NimbleThemeProviderBaseDirective`](https://github.com/ni/nimble/commit/0b088a67af4a860fce17003e37ea1bf8dfd10e8b#diff-a339dcfbd672a173f9daf1b67e96ed59cd7de2167935aae338d76a277476b026): Similar to the nimble-components version, this exposes properties for all of the Nimble labels. This allows apps to pick & choose to only localize a few labels (e.g. if they're only using a subset of the controls), or override specific label values (set on an ancestor theme provider, or override the values from the *InitializeAllLabels directive). However the app wouldn't automatically pick up new strings if they only set labels this way. *Possible Alternatives:* +- Create separate directives for each component that defines labels. Each of these directives could still target `nimble-theme-provider` in their selector. With this model, each directive would probably have the `$localize` properties for the labels for just that component (instead of a shared `NimbleLabels` class). This would allow apps to only pull in the labels for components they're using. However this would be much more difficult to codegen (we'd want the directives to be part of the modules for each component). - Codegen an Angular component that contains a `nimble-theme-provider`, sets the label attributes in the component HTML, adds `i18n-` versions of the attributes, and contains ``, then apps would use this component instead of nimble-theme-provider. Not prototyped so not sure if this is feasible or not. - Same as the previous idea, but the component is manually created in systemlink-lib-angular. This should work fine, but since it's not in Nimble / not codegen'd from the strings, it would be a manual process to update it with new Nimble labels in new nimble-angular versions. From cb841b0e11128a0784bd82d09eec935709d56848 Mon Sep 17 00:00:00 2001 From: Malcolm Smith <20709258+msmithNI@users.noreply.github.com> Date: Thu, 8 Jun 2023 19:04:10 -0500 Subject: [PATCH 04/12] HLD updates --- specs/labels-and-localization/README.md | 265 +++++++++++++++++------- 1 file changed, 185 insertions(+), 80 deletions(-) diff --git a/specs/labels-and-localization/README.md b/specs/labels-and-localization/README.md index 19ca508a90..12a13aeeea 100644 --- a/specs/labels-and-localization/README.md +++ b/specs/labels-and-localization/README.md @@ -13,10 +13,12 @@ We want to have consistent guidance for when & how to use labels for accessible ## Accessible Labels For many Nimble components, we already have a sufficient strategy for accessible labels. Guidance for specific components is documented in [nimble-components/docs/accessibility.md](../../packages/nimble-components/docs/accessibility.md). -Issue #1090 primarily covers the various icons used in Nimble components. A summary of our plan for accessible labels for icons currently used in Nimble components is: +[Issue #1090](https://github.com/ni/nimble/issues/1090) primarily covers the various icons used in Nimble components. A summary of our plan for accessible labels for icons currently used in Nimble components is: - Increment/decrement buttons in `nimble-number-field` ([#617](https://github.com/ni/nimble/issues/617)): These are `nimble-button` with `content-hidden`, and "Increment" and "Decrement" text. That approach is sufficient, but the text needs localization. - `nimble-banner` and `nimble-tooltip` severity icons: No accessible label specifically for the severity icons is currently planned. The banner/tooltip text should usually be sufficient to indicate if the message is an error or informational, so additional "Warning" or "Error" labels would usually be redundant. For the banner, the accessible text comes from the `title`. If we need to specifically support high-severity errors that we want to call the user's attention to, we could consider using the ARIA `role=alert` in that case. -- `nimble-banner` dismiss button icon: `nimble-button` with `content-hidden`, text comes from `dismissButtonLabel`, with a fallback as "Close". That approach is sufficient. `dismissButtonLabel` should be localized by the clients, the "Close" fallback text is Nimble-provided and needs localization. +- `nimble-banner` dismiss button icon: `nimble-button` with `content-hidden`, text comes from `dismissButtonLabel`, with a fallback as "Close". + - The "Close" fallback text is Nimble-provided and needs localization. + - We will probably remove `dismissButtonLabel` in the future, as all banners would generally use the same dismiss label text, so a per-element property isn't needed for it. The Nimble table will have many labels, which are summarized here: - Expand/Collapse Group button, Collapse All buttons: These are already `nimble-button` with `content-hidden`, but currently contain no text. We need to add text content for them (which will need localization). @@ -33,117 +35,158 @@ Our general approach to localization is that clients will handle localizing stri ### Plan for Nimble-Provided Labels -Conceptually the localized strings are very similar to the sharing pattern of Design Tokens. The vast majority of the time, you want to use the same value (i.e. numeric increment/decrement button text or collapse button text is not control instance specific) but want to adjust to a global config, i.e. theme or language. But also want to be able to override for specific controls as needed. - -We will create non-CSS-property design tokens, similar to the `theme` and `direction` design tokens. The values can be set via the `nimble-theme-provider`: +Conceptually the localized strings are very similar to the sharing pattern of Design Tokens. The vast majority of the time, you want to use the same value (i.e. numeric increment/decrement button text is not control instance specific). However, we do want to be able to override the strings for specific controls as needed. +We will create non-CSS-property design tokens, similar to the `theme` and `direction` design tokens. +We will also create new `nimble-i18n-*` elements with APIs for setting the tokens. These will generally be direct children of the `nimble-theme-provider`, and the theme provider will be the target that the token values are set on. ```html - - + + + ``` - Pros: - -- Easy discovery of localized string configuration. They are all on the theme-provider. - Can configure once for the whole page in one spot, does not need to be per control. Much fewer duplicated attributes / slotted elements in the page. -- Can override for specific elements as needed by wrapping in a theme-provider. +- Can override for specific elements as needed (by wrapping in `nimble-theme-provider` and `nimble-i18n-*` elements) - Can configure to be attribute or slotted on the theme provider. But if don't need mixed content then attribute makes sense. -- User provided control-specific content should still be provided via attributes / slots. This is for control provided content (the table control defines the collapse button, not the user). - -Cons: +- User provided control-specific content should still be provided via attributes / slots. This is for control provided content (i.e. the table control defines the collapse button, not the user). +- We can make more granular i18n providers, even down to the level of individual controls, if we're concerned about the number of strings apps would be automatically pulling in. (Note that currently we're just planning to have 2 though.) +- Code outside of Nimble could use the same concept (and derive from our i18n base class). If we end up with new controls in Nimble that are mostly specific to specific apps, we can also create new i18n providers for those elements, rather than sticking them all in the `i18n-core` element. +Cons +- More verbose than putting the APIs directly on nimble-theme-provider (but that's not really extensible) - Still somewhat verbose in the page, to have all of the localized strings as attributes - We can consider using the `fromView` attribute mode on the attributes for the strings on the theme-provider. If clients set the strings using the theme-provider properties rather than as attributes in the HTML (as is currently proposed for Angular), `fromView` would not reflect those strings back to the DOM. +- Apps may be pulling in more strings than they need (i.e. if they just use the banner, they'll pick up the rest of the strings in `i18n-core`). Mitigation is to split off large sets of strings (e.g. those for the table). - Not an originally intended use case for DesignToken -- Only nimble-components defined within Nimble can add new attributes to the theme-provider. - - We're encouraging clients to add new components to Nimble, including custom column types for the table, and those can use this approach - - Components outside Nimble could still have accessible labels by defining attributes on their components for them + +Other notes: +- We don't expect to need mixed content (i.e. other than simple strings), so attributes should be sufficient (vs. slots) +- If the page was automatically translated by something like Google Translate, the attributes on the i18n providers don't get translated. However we think this is OK because the expected usages of the labels (button content, slotted content, etc) would get translated. + +The current set of known labels for Nimble is shown below: + +**nimble-i18n-core** +| Token Name | English string | +|------------------------|----------------| +| banner-dismiss | Close | +| number-field-increment | Increment | +| number-field-decrement | Decrement | + +**nimble-i18n-table** +| Token Name | English string | +|---------------------------------------|---------------------| +| table-group-collapse | Collapse group | +| table-group-expand | Expand group | +| table-groups-collapse-all | Collapse all groups | +| table-cell-action-menu-label | Options | +| table-column-header-menu | Column Options | +| table-column-header-grouped-indicator | Grouped | +| table-column-sort-ascending | Sort ascending | +| table-column-sort-descending | Sort descending | +| table-column-group-by | Group by | +| table-column-size-to-content | Size to content | + +Note: We will probably remove the `table` prefix from the properties and attribute names on `nimble-i18n-table` as it's redundant. `table-cell-action-menu-label` is a fallback for when column.actionMenuLabel is unset. + +The expected format for token names is: +- component name, e.g. `number-field` or `table` +- component part/category (optional), e.g. `column-header` +- specific functionality or sub-part, e.g. `decrement` +- the suffix `label` (will be omitted from the i18n properties/attributes) + +Example: +```ts +export const numberFieldIncrementLabel = DesignToken.create({ + name: 'number-field-increment-label', + cssCustomPropertyName: null +}).withDefault('Increment'); +// on the i18n element: +@attr({ attribute: 'number-field-increment' }) +public numberFieldIncrement = 'Increment'; +``` #### Implementation Details -See the prototype branch: [localizable-labels-prototype](https://github.com/ni/nimble/commit/0b088a67af4a860fce17003e37ea1bf8dfd10e8b) +See the prototype branch: [localizable-labels-prototype-2](https://github.com/ni/nimble/compare/%40ni/nimble-angular_v16.6.3...localizable-labels-prototype-2?expand=1) **nimble-components** -We'll create a file containing the label IDs and English string values. A simple version of this is a JSON file (Prototype: [`labels.json`](https://github.com/ni/nimble/commit/0b088a67af4a860fce17003e37ea1bf8dfd10e8b#diff-0e77bd18c80fb68ad9624a1eeccbec4a539f98b92e2ff97a013f763cc897a8ce)): -```json -{ - "labelNumberFieldIncrement": "Increment", - "labelNumberFieldDecrement": "Decrement", - ... -} -``` -We'll code-gen source files from that (Prototype: [`build/generate-labels/`](https://github.com/ni/nimble/commit/0b088a67af4a860fce17003e37ea1bf8dfd10e8b#diff-7eb7cdf375d9cf88a5e1083a264b299d3143779ce57906cc42aaa408df354f1a)): -- For each label, an exported `DesignToken` for it (with default of the English string). Prototype: [`labels/index.ts`](https://github.com/ni/nimble/commit/0b088a67af4a860fce17003e37ea1bf8dfd10e8b#diff-67d489e618f4180c3d10019eb8252b7b13a5f52516585c6baba2e4fc6d5b9914) -- Collections of the label names and values, which can be used in subsequent builds / Storybook stories. Prototype: `label-names.ts` and `label-values.ts` -- A `ThemeProviderBase` class with `@attr` properties and `Changed()` methods for each label, which ThemeProvider will derive from. Prototype: [`theme-provider-base.ts`](https://github.com/ni/nimble/commit/0b088a67af4a860fce17003e37ea1bf8dfd10e8b#diff-e90f67f575dfcd6fd5835a58a2663c8d1f05c0837328902751c8759fac817df9). (This may change slightly with the proposed `nimble-global-theme-provider` from PR 1257. The expectation is that both ThemeProvider and the GlobalThemeProvider would have the same attributes/properties to configure the labels.) +We'll define a base class (prototype: [i18n-base.ts](https://github.com/ni/nimble/blob/b13117639de55db3086561edccc4dfe5994f9829/packages/nimble-components/src/i18n/i18n-base.ts)) for the i18n providers, which handles setting the token values on the ancestor theme-provider. For each i18n provider, we'll have a file declaring the DesignTokens, with a class deriving from the base class that has attributes+properties for setting the token values (prototype: [i18n/core](https://github.com/ni/nimble/blob/b13117639de55db3086561edccc4dfe5994f9829/packages/nimble-components/src/i18n/core/index.ts) and [i18n/table](https://github.com/ni/nimble/blob/b13117639de55db3086561edccc4dfe5994f9829/packages/nimble-components/src/i18n/table/index.ts)). **nimble-angular** -One goal for the nimble-angular label support is to make it easy (or automatic) for clients to pick up new localized strings/labels when they uptake new nimble-angular versions. The way the prototype accomplishes that is: -- Export (codegen) a [`NimbleLabels` class](https://github.com/ni/nimble/commit/0b088a67af4a860fce17003e37ea1bf8dfd10e8b#diff-8ada6f05dc0978bee3e574b5e06167631a98b8fd1ac8f5467aa3828e3ba933f1) that has properties for all Nimble labels, and uses the Angular `localize` function: -```ts -export class NimbleLabels { - public static readonly labelNumberFieldIncrement = $localize`Increment`; - public static readonly labelNumberFieldDecrement = $localize`Decrement`; -} -``` -(We may encode the label ID as the description so it shows up in the message files, i.e. ``$localize`:nimble-label-number-field-decrement:Decrement` ``). -If a consumer of nimble-angular imports the module containing the `NimbleLabels` class, once they run `ng extract-i18n` for their app, the message files will then contain the Nimble strings for translation. Additionally, once a client has done that, labels/strings in future nimble-angular version updates would automatically get pulled in by `ng extract-i18n` if it was run again after a package update. -- Export (codegen) a directive which will automatically set all of the labels on a Nimble theme provider, with the values from `NimbleLabels`. In the prototype this is [`NimbleThemeProviderInitializeAllLabelsDirective`](https://github.com/ni/nimble/commit/0b088a67af4a860fce17003e37ea1bf8dfd10e8b#diff-a8bb0152269fed064bdc90ef28541bdedaeaf86f3a4e2b1751d2da68a6090aca) with selector `nimble-theme-provider[initializeAllLabels]`. Assuming that an app is OK with pulling in and localizing all of Nimble's strings/labels, using this directive will make the process of using those localized strings mostly automatic. -- Codegen [`NimbleThemeProviderBaseDirective`](https://github.com/ni/nimble/commit/0b088a67af4a860fce17003e37ea1bf8dfd10e8b#diff-a339dcfbd672a173f9daf1b67e96ed59cd7de2167935aae338d76a277476b026): Similar to the nimble-components version, this exposes properties for all of the Nimble labels. This allows apps to pick & choose to only localize a few labels (e.g. if they're only using a subset of the controls), or override specific label values (set on an ancestor theme provider, or override the values from the *InitializeAllLabels directive). However the app wouldn't automatically pick up new strings if they only set labels this way. +Each i18n provider will have its own Angular directive and module (prototype: [nimble-i18n-core.directive](https://github.com/ni/nimble/blob/b13117639de55db3086561edccc4dfe5994f9829/angular-workspace/projects/ni/nimble-angular/src/directives/i18n/core/nimble-i18n-core.directive.ts) and [nimble-i18n-core.module](https://github.com/ni/nimble/blob/b13117639de55db3086561edccc4dfe5994f9829/angular-workspace/projects/ni/nimble-angular/src/directives/i18n/core/nimble-i18n-core.module.ts) for `i18n-core`.) -*Possible Alternatives:* -- Create separate directives for each component that defines labels. Each of these directives could still target `nimble-theme-provider` in their selector. With this model, each directive would probably have the `$localize` properties for the labels for just that component (instead of a shared `NimbleLabels` class). This would allow apps to only pull in the labels for components they're using. However this would be much more difficult to codegen (we'd want the directives to be part of the modules for each component). -- Codegen an Angular component that contains a `nimble-theme-provider`, sets the label attributes in the component HTML, adds `i18n-` versions of the attributes, and contains ``, then apps would use this component instead of nimble-theme-provider. Not prototyped so not sure if this is feasible or not. -- Same as the previous idea, but the component is manually created in systemlink-lib-angular. This should work fine, but since it's not in Nimble / not codegen'd from the strings, it would be a manual process to update it with new Nimble labels in new nimble-angular versions. - -**nimble-blazor** -Label support for Nimble Blazor is more rudimentary than nimble-angular. Additionally, the prototype doesn't yet codegen for Blazor, but it has some hardcoded changes to illustrate the concepts: -- [`NimbleBlazor\NimbleResources\NimbleLabels.resx`](https://github.com/ni/nimble/commit/0b088a67af4a860fce17003e37ea1bf8dfd10e8b#diff-cfca56c824b9b81654e41d45429aa8dad759211ca1ceb8e5bac7886b75bbeab4): Contains labels and English values, i.e. `labelNumberFieldIncrement` => `'Increment'` (this would be codegen'd) - - In the [csproj](https://github.com/ni/nimble/commit/0b088a67af4a860fce17003e37ea1bf8dfd10e8b#diff-3635dce04cb0a44d829f97259dce31b8f2abf8d0e80a4c5ccf9506babc93a78aR38), the Build Action for this is Content (not EmbeddedResource), `copyToOutput=true`, `IncludeInPackage=true`. This results in the resx file being included in the NuGet, in [contentFiles/](https://learn.microsoft.com/en-us/nuget/reference/nuspec#using-the-contentfiles-element-for-content-files) -- [`Components\NimbleThemeProvider.razor`](https://github.com/ni/nimble/commit/0b088a67af4a860fce17003e37ea1bf8dfd10e8b#diff-935610f630ad061236774e447badf29f08dd1467f6c4c6e45ead7214641afe6d): Add code/markup for labels: i.e. `label-number-field-increment="@LabelNumberFieldIncrement"` in the Razor file, `[Parameter] public string? LabelNumberFieldIncrement { get; set; }` in the C# code. We may need to fully codegen the Razor file to add the attribute bindings. +In order to make it easy/automatic for clients to pick up new localized strings/labels when they uptake new nimble-angular versions, each i18n provider has an additional directive that will set all of the Nimble-defined labels/strings, using Angular's `$localize` function on the English strings. +Prototype: [nimble-i18n-core-with-defaults.directive](https://github.com/ni/nimble/blob/b13117639de55db3086561edccc4dfe5994f9829/angular-workspace/projects/ni/nimble-angular/src/directives/i18n/core/nimble-i18n-core.module.ts) -Once clients add a reference to the NuGet, `NimbleResources\NimbleLabels.resx` will appear in their project as a link (to that file in the NuGet). At that point, the remaining process is somewhat convoluted, and would need documentation. The prototype branch has done some of this in `Demo.Shared` and `Demo.Server`: -- Browse to that resx file on disk, and manually copy it into the app project directory, in `Resources\` -- Add it to the Blazor project (as Build Action = Embedded Resource). Then, open it in the resx editor, and set Access Modifier = Public or Internal (to generate the `.designer.cs` file) -- To translate/localize the resources, clone the resx file (e.g. `NimbleLabels.fr.resx`) and translate the values in it. The app also needs to follow the [Microsoft Blazor localization docs](https://learn.microsoft.com/en-us/aspnet/core/blazor/globalization-localization?view=aspnetcore-7.0&pivots=server) in terms of opting into localization, configuring the supported locales, etc. -- To consume the resources, in the Razor file using the NimbleThemeProvider, add: -```cs -[Inject] -internal IStringLocalizer? LabelProvider { get; set; } -``` -Then bind to the theme provider labels in the Razor template: -```xml - +For each i18n provider that an Angular app will use: +- The app imports that specific i18n module. This will result in the app pulling in the Nimble-provided labels/strings for localization when they run `ng extract-i18n`, due to the `with-defaults.directive` using `$localize`. When they pull in new nimble-angular versions in the future, new strings in the same i18n provider will automatically get pulled in too. +- The app adds that i18n element as a child to their theme provider: +```html + + + ``` +- If the app needs to customize any of the labels, they can do so via the i18n directive API. Generally the root i18n provider would use `withDefaults` to set all the labels to their localized values, and any nested ones would not. -The process above isn't great. When updated versions of Nimble Blazor are released, if they have new labels, the consuming app would need to repeat the process of manually copying the resx, and translating it. They'd also need to manually add the new label properties in their Razor file using the NimbleThemeProvider. +Codegen: We should be able to codegen the Angular directives. The prototype does not, for simplicity. -If we have any clients that will be using Nimble Blazor and non-English locales we should probably do additional research to see if we can come up with a more seamless approach. +**nimble-blazor** +We currently don't have a good solution for Blazor clients to automatically pick up or localize our labels/strings. +We do still plan to create Razor components for each i18n provider, so that Blazor clients can manually specify/localize the labels if desired. +(Prototype: [NimbleI18nCore.razor](https://github.com/ni/nimble/compare/%40ni/nimble-angular_v16.6.3...localizable-labels-prototype-2?expand=1#diff-88863ebb8b90aab301573eeb66b6850c26327d12be6b0fa33bcd3cccaadca938) for `i18n-core`). -*Possible Alternatives:* -- Add an `IStringLocalizer` property to the `NimbleThemeProvider` Razor component, and codegen the `Label*` properties to use it if it's set. This would improve the process of telling Nimble about the localized resources, but wouldn't change the process of manually copying the resx to start with. -- Define an MSBuild task that copies the resx file, similar to what's outlined in [this GitHub comment](https://github.com/ni/nimble/issues/558#issuecomment-1129279985). Not much better / still a manual process. +Codegen: We should be able to codegen the Blazor components. The prototype does not, for simplicity. + +If we have any clients that will be using Nimble Blazor and non-English locales, we should probably do additional research to see if we can come up with a more seamless approach. Note that the `i18n-core` labels are not visible / are for accessibility only, so this may only be a priority for clients using the Nimble table (which will have visible strings needing localization). ### Plan for Client-Provided Labels -Examples: Button content, menu item content, `nimble-banner` `dismissButtonLabel`, `nimble-table` column `action-menu-label` +Examples: Button content, menu item content Almost all of these labels are context-specific, i.e. different buttons on the page will have different text, and different columns will have different action menu labels. Clients will localize those labels themselves: - For Angular, they can use the `localize` function and/or `i18n` attributes/ attribute prefixes -- For Blazor, they can use `.resx` files and `IStringLocalizer` +- For Blazor, they could use `.resx` files and `IStringLocalizer` -The banner's `dismissButtonLabel` will be semi-redundant once we have a Nimble-provided label for banner dismiss buttons, so we could consider removing that attribute in the future. +The banner's `dismissButtonLabel` will be redundant once we have a Nimble-provided label for banner dismiss buttons, so we will probably remove that attribute in the future. ## Alternative Implementations / Designs -Alternative options for Nimble-provided labels: -### Slots +**Alternative options for Nimble-provided labels:** + +### Putting the token APIs directly on theme-provider + +```html + + + +``` + +Pros: + +- Easy discovery of localized string configuration. They are all on the theme-provider. +- Can configure once for the whole page in one spot, does not need to be per control. Much fewer duplicated attributes / slotted elements in the page. +- Can override for specific elements as needed by wrapping in a theme-provider. +- Can configure to be attribute or slotted on the theme provider. But if don't need mixed content then attribute makes sense. +- User provided control-specific content should still be provided via attributes / slots. This is for control provided content (the table control defines the collapse button, not the user). + +Cons: + +- Only nimble-components defined within Nimble can add new attributes to the theme-provider. + - We're encouraging clients to add new components to Nimble, including custom column types for the table, and those can use this approach + - Components outside Nimble could still have accessible labels by defining attributes on their components for them + +### Slots on each element Create slots per localized string on the element. @@ -164,7 +207,7 @@ Cons: - DOM gets littered with many additional elements for providing localized strings - Lots of duplicated slotted content / elements. Have to emit into every table element as children. -### Attributes +### Attributes on each element Create attribute per localized string on the element. @@ -184,7 +227,69 @@ Cons: - Lots of duplicated attributes. Have to emit into every table element as children. - Only text content (maybe that's okay) +### Define labels on span elements to aid automatic translation +Under the current proposals, the labels are specified as attributes on the i18n elements. If a user translates the page with Google Translate, the attributes will not be translated. However all of our current usages of the labels (as button text, menu items, slotted content) would still be translated. + +If we wanted to ensure our labels on the i18n providers could be automatically translated, we might want to put them in translatable elements like `span`s: +```html + + Increment + +``` + +We could still add on that approach in the future, for specific labels, if we identify any that would only be used by canvas-drawn elements (i.e. wafer map). + +**Alternative implementation options:** + +### Codegen more of the code (including nimble-components) +See the prototype branch: [localizable-labels-prototype](https://github.com/ni/nimble/commit/0b088a67af4a860fce17003e37ea1bf8dfd10e8b). Note that this prototype had all of the labels on the theme provider itself. + +*nimble-components* +We could define the labels in a JSON file (Prototype: [`labels.json`](https://github.com/ni/nimble/commit/0b088a67af4a860fce17003e37ea1bf8dfd10e8b#diff-0e77bd18c80fb68ad9624a1eeccbec4a539f98b92e2ff97a013f763cc897a8ce)): +```json +{ + "labelNumberFieldIncrement": "Increment", + "labelNumberFieldDecrement": "Decrement", + ... +} +``` +From that we could codegen (Prototype: [`build/generate-labels/`](https://github.com/ni/nimble/commit/0b088a67af4a860fce17003e37ea1bf8dfd10e8b#diff-7eb7cdf375d9cf88a5e1083a264b299d3143779ce57906cc42aaa408df354f1a)): +- The label design tokens (Prototype: `labels/`), and APIs for them on the theme provider (Prototype: `theme-provider-base.ts`) + +### Other options for nimble-angular +- Create separate directives for each component that defines labels. Each of these directives could still target `nimble-theme-provider` in their selector. With this model, each directive would probably have the `$localize` properties for the labels for just that component. This would allow apps to only pull in the labels for components they're using. However this would be much more difficult to codegen (we'd want the directives to be part of the modules for each component). +- Codegen an Angular component that contains a `nimble-theme-provider`, sets the label attributes in the component HTML, adds `i18n-` versions of the attributes, and contains ``, then apps would use this component instead of nimble-theme-provider. Not prototyped so not sure if this is feasible or not. +- Same as the previous idea, but the component is manually created in systemlink-lib-angular. This should work fine, but since it's not in Nimble / not codegen'd from the strings, it would be a manual process to update it with new Nimble labels in new nimble-angular versions. + +### Provide a .resx file for Blazor +Prototype and description of this approach: +- [`NimbleBlazor\NimbleResources\NimbleLabels.resx`](https://github.com/ni/nimble/commit/0b088a67af4a860fce17003e37ea1bf8dfd10e8b#diff-cfca56c824b9b81654e41d45429aa8dad759211ca1ceb8e5bac7886b75bbeab4): Contains labels and English values, i.e. `labelNumberFieldIncrement` => `'Increment'` (this would be codegen'd) + - In the [csproj](https://github.com/ni/nimble/commit/0b088a67af4a860fce17003e37ea1bf8dfd10e8b#diff-3635dce04cb0a44d829f97259dce31b8f2abf8d0e80a4c5ccf9506babc93a78aR38), the Build Action for this is Content (not EmbeddedResource), `copyToOutput=true`, `IncludeInPackage=true`. This results in the resx file being included in the NuGet, in [contentFiles/](https://learn.microsoft.com/en-us/nuget/reference/nuspec#using-the-contentfiles-element-for-content-files) +- [`Components\NimbleThemeProvider.razor`](https://github.com/ni/nimble/commit/0b088a67af4a860fce17003e37ea1bf8dfd10e8b#diff-935610f630ad061236774e447badf29f08dd1467f6c4c6e45ead7214641afe6d): Add code/markup for labels: i.e. `label-number-field-increment="@LabelNumberFieldIncrement"` in the Razor file, `[Parameter] public string? LabelNumberFieldIncrement { get; set; }` in the C# code. We may need to fully codegen the Razor file to add the attribute bindings. + +Once clients add a reference to the NuGet, `NimbleResources\NimbleLabels.resx` will appear in their project as a link (to that file in the NuGet). At that point, the remaining process is somewhat convoluted, and would need documentation. The prototype branch has done some of this in `Demo.Shared` and `Demo.Server`: +- Browse to that resx file on disk, and manually copy it into the app project directory, in `Resources\` +- Add it to the Blazor project (as Build Action = Embedded Resource). Then, open it in the resx editor, and set Access Modifier = Public or Internal (to generate the `.designer.cs` file) +- To translate/localize the resources, clone the resx file (e.g. `NimbleLabels.fr.resx`) and translate the values in it. The app also needs to follow the [Microsoft Blazor localization docs](https://learn.microsoft.com/en-us/aspnet/core/blazor/globalization-localization?view=aspnetcore-7.0&pivots=server) in terms of opting into localization, configuring the supported locales, etc. +- To consume the resources, in the Razor file using the NimbleThemeProvider, add: +```cs +[Inject] +internal IStringLocalizer? LabelProvider { get; set; } +``` +Then bind to the theme provider labels in the Razor template: +```xml + +``` + +The process above isn't great. When updated versions of Nimble Blazor are released, if they have new labels, the consuming app would need to repeat the process of manually copying the resx, and translating it. They'd also need to manually add the new label properties in their Razor file using the NimbleThemeProvider. + +*Possible Alternatives:* +- Add an `IStringLocalizer` property to the `NimbleThemeProvider` Razor component, and codegen the `Label*` properties to use it if it's set. This would improve the process of telling Nimble about the localized resources, but wouldn't change the process of manually copying the resx to start with. +- Define an MSBuild task that copies the resx file, similar to what's outlined in [this GitHub comment](https://github.com/ni/nimble/issues/558#issuecomment-1129279985). Not much better / still a manual process. + ## Open Issues +- Naming + - Do we like having `i18n` in the element names, or should we pick something else? Once camel-cased it also looks strange, i.e. `NimbleI18nCore.razor` +- Best way to document this system (in Storybook)? - Are the label IDs (`label-number-field-increment`) sufficient information for someone to translate the strings from? (Should we include an additional/optional description with each label/string?) -- Does Nimble need to support more advanced use cases of strings/labels like format specifiers or [ICU expressions](https://angular.io/guide/i18n-common-prepare#icu-expressions)? From 5468d00d15b0c0525a111ae624f9def38e276717 Mon Sep 17 00:00:00 2001 From: Malcolm Smith <20709258+msmithNI@users.noreply.github.com> Date: Thu, 8 Jun 2023 19:12:20 -0500 Subject: [PATCH 05/12] Cleanup / fix link --- specs/labels-and-localization/README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/specs/labels-and-localization/README.md b/specs/labels-and-localization/README.md index 12a13aeeea..64fc5ae386 100644 --- a/specs/labels-and-localization/README.md +++ b/specs/labels-and-localization/README.md @@ -53,8 +53,6 @@ We will also create new `nimble-i18n-*` elements with APIs for setting the token Pros: - Can configure once for the whole page in one spot, does not need to be per control. Much fewer duplicated attributes / slotted elements in the page. - Can override for specific elements as needed (by wrapping in `nimble-theme-provider` and `nimble-i18n-*` elements) -- Can configure to be attribute or slotted on the theme provider. But if don't need mixed content then attribute makes sense. -- User provided control-specific content should still be provided via attributes / slots. This is for control provided content (i.e. the table control defines the collapse button, not the user). - We can make more granular i18n providers, even down to the level of individual controls, if we're concerned about the number of strings apps would be automatically pulling in. (Note that currently we're just planning to have 2 though.) - Code outside of Nimble could use the same concept (and derive from our i18n base class). If we end up with new controls in Nimble that are mostly specific to specific apps, we can also create new i18n providers for those elements, rather than sticking them all in the `i18n-core` element. @@ -122,7 +120,7 @@ We'll define a base class (prototype: [i18n-base.ts](https://github.com/ni/nimbl Each i18n provider will have its own Angular directive and module (prototype: [nimble-i18n-core.directive](https://github.com/ni/nimble/blob/b13117639de55db3086561edccc4dfe5994f9829/angular-workspace/projects/ni/nimble-angular/src/directives/i18n/core/nimble-i18n-core.directive.ts) and [nimble-i18n-core.module](https://github.com/ni/nimble/blob/b13117639de55db3086561edccc4dfe5994f9829/angular-workspace/projects/ni/nimble-angular/src/directives/i18n/core/nimble-i18n-core.module.ts) for `i18n-core`.) In order to make it easy/automatic for clients to pick up new localized strings/labels when they uptake new nimble-angular versions, each i18n provider has an additional directive that will set all of the Nimble-defined labels/strings, using Angular's `$localize` function on the English strings. -Prototype: [nimble-i18n-core-with-defaults.directive](https://github.com/ni/nimble/blob/b13117639de55db3086561edccc4dfe5994f9829/angular-workspace/projects/ni/nimble-angular/src/directives/i18n/core/nimble-i18n-core.module.ts) +Prototype: [nimble-i18n-core-with-defaults.directive](https://github.com/ni/nimble/blob/b13117639de55db3086561edccc4dfe5994f9829/angular-workspace/projects/ni/nimble-angular/src/directives/i18n/core/nimble-i18n-core-with-defaults.directive.ts) For each i18n provider that an Angular app will use: - The app imports that specific i18n module. This will result in the app pulling in the Nimble-provided labels/strings for localization when they run `ng extract-i18n`, due to the `with-defaults.directive` using `$localize`. When they pull in new nimble-angular versions in the future, new strings in the same i18n provider will automatically get pulled in too. From 83922150c6f68256fe5f5c1315c74ceda715b062 Mon Sep 17 00:00:00 2001 From: Malcolm Smith <20709258+msmithNI@users.noreply.github.com> Date: Thu, 8 Jun 2023 19:22:55 -0500 Subject: [PATCH 06/12] Update prototype links --- specs/labels-and-localization/README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/specs/labels-and-localization/README.md b/specs/labels-and-localization/README.md index 64fc5ae386..a05959f9a8 100644 --- a/specs/labels-and-localization/README.md +++ b/specs/labels-and-localization/README.md @@ -117,14 +117,14 @@ See the prototype branch: [localizable-labels-prototype-2](https://github.com/ni We'll define a base class (prototype: [i18n-base.ts](https://github.com/ni/nimble/blob/b13117639de55db3086561edccc4dfe5994f9829/packages/nimble-components/src/i18n/i18n-base.ts)) for the i18n providers, which handles setting the token values on the ancestor theme-provider. For each i18n provider, we'll have a file declaring the DesignTokens, with a class deriving from the base class that has attributes+properties for setting the token values (prototype: [i18n/core](https://github.com/ni/nimble/blob/b13117639de55db3086561edccc4dfe5994f9829/packages/nimble-components/src/i18n/core/index.ts) and [i18n/table](https://github.com/ni/nimble/blob/b13117639de55db3086561edccc4dfe5994f9829/packages/nimble-components/src/i18n/table/index.ts)). **nimble-angular** -Each i18n provider will have its own Angular directive and module (prototype: [nimble-i18n-core.directive](https://github.com/ni/nimble/blob/b13117639de55db3086561edccc4dfe5994f9829/angular-workspace/projects/ni/nimble-angular/src/directives/i18n/core/nimble-i18n-core.directive.ts) and [nimble-i18n-core.module](https://github.com/ni/nimble/blob/b13117639de55db3086561edccc4dfe5994f9829/angular-workspace/projects/ni/nimble-angular/src/directives/i18n/core/nimble-i18n-core.module.ts) for `i18n-core`.) +Each i18n provider will have its own Angular directive and module (prototype: [nimble-i18n-core.directive](https://github.com/ni/nimble/blob/d51ee14dc49db7070e5cab726c225f69635de17b/angular-workspace/projects/ni/nimble-angular/src/directives/i18n/core/nimble-i18n-core.directive.ts) and [nimble-i18n-core.module](https://github.com/ni/nimble/blob/d51ee14dc49db7070e5cab726c225f69635de17b/angular-workspace/projects/ni/nimble-angular/src/directives/i18n/core/nimble-i18n-core.module.ts) for `i18n-core`.) In order to make it easy/automatic for clients to pick up new localized strings/labels when they uptake new nimble-angular versions, each i18n provider has an additional directive that will set all of the Nimble-defined labels/strings, using Angular's `$localize` function on the English strings. -Prototype: [nimble-i18n-core-with-defaults.directive](https://github.com/ni/nimble/blob/b13117639de55db3086561edccc4dfe5994f9829/angular-workspace/projects/ni/nimble-angular/src/directives/i18n/core/nimble-i18n-core-with-defaults.directive.ts) +Prototype: [nimble-i18n-core-with-defaults.directive](https://github.com/ni/nimble/blob/d51ee14dc49db7070e5cab726c225f69635de17b/angular-workspace/projects/ni/nimble-angular/src/directives/i18n/core/nimble-i18n-core-with-defaults.directive.ts) For each i18n provider that an Angular app will use: -- The app imports that specific i18n module. This will result in the app pulling in the Nimble-provided labels/strings for localization when they run `ng extract-i18n`, due to the `with-defaults.directive` using `$localize`. When they pull in new nimble-angular versions in the future, new strings in the same i18n provider will automatically get pulled in too. -- The app adds that i18n element as a child to their theme provider: +- The app imports that specific i18n module (prototype: [in example app module](https://github.com/ni/nimble/blob/d51ee14dc49db7070e5cab726c225f69635de17b/angular-workspace/projects/example-client-app/src/app/app.module.ts#L73)). This will result in the app pulling in the Nimble-provided labels/strings for localization when they run `ng extract-i18n`, due to the `with-defaults.directive` using `$localize`. When they pull in new nimble-angular versions in the future, new strings in the same i18n provider will automatically get pulled in too. (Prototype: [messages.xlf](https://github.com/ni/nimble/blob/d51ee14dc49db7070e5cab726c225f69635de17b/angular-workspace/projects/example-client-app/src/locales/messages.xlf), output of `ng extract-i18n` after importing Nimble i18n core module) +- The app adds that i18n element as a child to their theme provider ([prototype](https://github.com/ni/nimble/blob/d51ee14dc49db7070e5cab726c225f69635de17b/angular-workspace/projects/example-client-app/src/app/app.component.html#L2)): ```html @@ -137,7 +137,7 @@ Codegen: We should be able to codegen the Angular directives. The prototype does **nimble-blazor** We currently don't have a good solution for Blazor clients to automatically pick up or localize our labels/strings. We do still plan to create Razor components for each i18n provider, so that Blazor clients can manually specify/localize the labels if desired. -(Prototype: [NimbleI18nCore.razor](https://github.com/ni/nimble/compare/%40ni/nimble-angular_v16.6.3...localizable-labels-prototype-2?expand=1#diff-88863ebb8b90aab301573eeb66b6850c26327d12be6b0fa33bcd3cccaadca938) for `i18n-core`). +(Prototype: [NimbleI18nCore.razor](https://github.com/ni/nimble/compare/@ni/nimble-angular_v16.6.3...localizable-labels-prototype-2?expand=1#diff-88863ebb8b90aab301573eeb66b6850c26327d12be6b0fa33bcd3cccaadca938) for `i18n-core`). Codegen: We should be able to codegen the Blazor components. The prototype does not, for simplicity. From bec959e67d422a03817573a8e75f032e8f404b9d Mon Sep 17 00:00:00 2001 From: Malcolm Smith <20709258+msmithNI@users.noreply.github.com> Date: Mon, 12 Jun 2023 15:14:58 -0500 Subject: [PATCH 07/12] Update README.md --- specs/labels-and-localization/README.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/specs/labels-and-localization/README.md b/specs/labels-and-localization/README.md index a05959f9a8..ee998795f1 100644 --- a/specs/labels-and-localization/README.md +++ b/specs/labels-and-localization/README.md @@ -66,6 +66,7 @@ Cons Other notes: - We don't expect to need mixed content (i.e. other than simple strings), so attributes should be sufficient (vs. slots) - If the page was automatically translated by something like Google Translate, the attributes on the i18n providers don't get translated. However we think this is OK because the expected usages of the labels (button content, slotted content, etc) would get translated. +- We may want to provide a description along with each English string, to aid in translation. The current set of known labels for Nimble is shown below: @@ -120,10 +121,14 @@ We'll define a base class (prototype: [i18n-base.ts](https://github.com/ni/nimbl Each i18n provider will have its own Angular directive and module (prototype: [nimble-i18n-core.directive](https://github.com/ni/nimble/blob/d51ee14dc49db7070e5cab726c225f69635de17b/angular-workspace/projects/ni/nimble-angular/src/directives/i18n/core/nimble-i18n-core.directive.ts) and [nimble-i18n-core.module](https://github.com/ni/nimble/blob/d51ee14dc49db7070e5cab726c225f69635de17b/angular-workspace/projects/ni/nimble-angular/src/directives/i18n/core/nimble-i18n-core.module.ts) for `i18n-core`.) In order to make it easy/automatic for clients to pick up new localized strings/labels when they uptake new nimble-angular versions, each i18n provider has an additional directive that will set all of the Nimble-defined labels/strings, using Angular's `$localize` function on the English strings. -Prototype: [nimble-i18n-core-with-defaults.directive](https://github.com/ni/nimble/blob/d51ee14dc49db7070e5cab726c225f69635de17b/angular-workspace/projects/ni/nimble-angular/src/directives/i18n/core/nimble-i18n-core-with-defaults.directive.ts) +Prototype: [nimble-i18n-core-with-defaults.directive](https://github.com/ni/nimble/blob/d51ee14dc49db7070e5cab726c225f69635de17b/angular-workspace/projects/ni/nimble-angular/src/directives/i18n/core/nimble-i18n-core-with-defaults.directive.ts) +If we define descriptions for each string, we can include it so it appears in the message files, such as: ``$localize`:Nimble number-field increment button label:Increment` ``. + +Once an Angular app uptakes the nimble-angular version that introduces these i18n modules, running `ng extract-i18n` will result in the app pulling in Nimble-provided labels/strings for localization. (Prototype: [messages.xlf](https://github.com/ni/nimble/blob/d51ee14dc49db7070e5cab726c225f69635de17b/angular-workspace/projects/example-client-app/src/locales/messages.xlf), output of `ng extract-i18n` after upgrading to this nimble-angular version) +When they pull in new nimble-angular versions in the future and re-run that command, the new strings will again be pulled in for translation automatically. For each i18n provider that an Angular app will use: -- The app imports that specific i18n module (prototype: [in example app module](https://github.com/ni/nimble/blob/d51ee14dc49db7070e5cab726c225f69635de17b/angular-workspace/projects/example-client-app/src/app/app.module.ts#L73)). This will result in the app pulling in the Nimble-provided labels/strings for localization when they run `ng extract-i18n`, due to the `with-defaults.directive` using `$localize`. When they pull in new nimble-angular versions in the future, new strings in the same i18n provider will automatically get pulled in too. (Prototype: [messages.xlf](https://github.com/ni/nimble/blob/d51ee14dc49db7070e5cab726c225f69635de17b/angular-workspace/projects/example-client-app/src/locales/messages.xlf), output of `ng extract-i18n` after importing Nimble i18n core module) +- The app imports that specific i18n module (prototype: [in example app module](https://github.com/ni/nimble/blob/d51ee14dc49db7070e5cab726c225f69635de17b/angular-workspace/projects/example-client-app/src/app/app.module.ts#L73)). - The app adds that i18n element as a child to their theme provider ([prototype](https://github.com/ni/nimble/blob/d51ee14dc49db7070e5cab726c225f69635de17b/angular-workspace/projects/example-client-app/src/app/app.component.html#L2)): ```html @@ -132,15 +137,13 @@ For each i18n provider that an Angular app will use: ``` - If the app needs to customize any of the labels, they can do so via the i18n directive API. Generally the root i18n provider would use `withDefaults` to set all the labels to their localized values, and any nested ones would not. -Codegen: We should be able to codegen the Angular directives. The prototype does not, for simplicity. +We can consider codegen-ing the Angular directives, which would let us avoid copy-pasting the English strings/ descriptions at the nimble-angular level, but at the expense of obfucscating some of the code (in the generator scripts). **nimble-blazor** We currently don't have a good solution for Blazor clients to automatically pick up or localize our labels/strings. We do still plan to create Razor components for each i18n provider, so that Blazor clients can manually specify/localize the labels if desired. (Prototype: [NimbleI18nCore.razor](https://github.com/ni/nimble/compare/@ni/nimble-angular_v16.6.3...localizable-labels-prototype-2?expand=1#diff-88863ebb8b90aab301573eeb66b6850c26327d12be6b0fa33bcd3cccaadca938) for `i18n-core`). -Codegen: We should be able to codegen the Blazor components. The prototype does not, for simplicity. - If we have any clients that will be using Nimble Blazor and non-English locales, we should probably do additional research to see if we can come up with a more seamless approach. Note that the `i18n-core` labels are not visible / are for accessibility only, so this may only be a priority for clients using the Nimble table (which will have visible strings needing localization). ### Plan for Client-Provided Labels @@ -290,4 +293,3 @@ The process above isn't great. When updated versions of Nimble Blazor are releas - Naming - Do we like having `i18n` in the element names, or should we pick something else? Once camel-cased it also looks strange, i.e. `NimbleI18nCore.razor` - Best way to document this system (in Storybook)? -- Are the label IDs (`label-number-field-increment`) sufficient information for someone to translate the strings from? (Should we include an additional/optional description with each label/string?) From cc9435c9eeb17cbc8e8bf12510d9f762890a9c8e Mon Sep 17 00:00:00 2001 From: Malcolm Smith <20709258+msmithNI@users.noreply.github.com> Date: Mon, 12 Jun 2023 15:18:53 -0500 Subject: [PATCH 08/12] Update README.md --- specs/labels-and-localization/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/specs/labels-and-localization/README.md b/specs/labels-and-localization/README.md index ee998795f1..51302be078 100644 --- a/specs/labels-and-localization/README.md +++ b/specs/labels-and-localization/README.md @@ -137,6 +137,8 @@ For each i18n provider that an Angular app will use: ``` - If the app needs to customize any of the labels, they can do so via the i18n directive API. Generally the root i18n provider would use `withDefaults` to set all the labels to their localized values, and any nested ones would not. +We expect most apps in SystemLink to consume both `nimble-i18n-core` and `nimble-i18n-table` as-is. + We can consider codegen-ing the Angular directives, which would let us avoid copy-pasting the English strings/ descriptions at the nimble-angular level, but at the expense of obfucscating some of the code (in the generator scripts). **nimble-blazor** From 22535048dca67ef49bacc49be030d707066ecbcd Mon Sep 17 00:00:00 2001 From: Malcolm Smith <20709258+msmithNI@users.noreply.github.com> Date: Mon, 12 Jun 2023 19:50:52 -0500 Subject: [PATCH 09/12] Update README.md --- specs/labels-and-localization/README.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/specs/labels-and-localization/README.md b/specs/labels-and-localization/README.md index 51302be078..b49feb62fb 100644 --- a/specs/labels-and-localization/README.md +++ b/specs/labels-and-localization/README.md @@ -159,6 +159,16 @@ Clients will localize those labels themselves: The banner's `dismissButtonLabel` will be redundant once we have a Nimble-provided label for banner dismiss buttons, so we will probably remove that attribute in the future. +## Documentation Updates + +We'll need to update our documentation to describe this new system: +- Storybook + - Per-component: If a component uses localizable labels, its Storybook docs should list the label names, and which `i18n` provider they're a part of. + - (Optional) Consider adding a new top-level page like `Concepts/Localization` which describes the `i18n` providers. We could also consider a single page that describes `nimble-theme-provider`, plus the `nimble-i18n-*` providers. As an alternative, we could make a `Strings/Labels` page under `Tokens` with sections for each `i18n` provider and the label names they contain, but that may be somewhat redundant if each component lists the label names too. +- nimble-components README.md: This will document the `i18n` providers and where they should go on the page, similiar to the current theme-provider documentation. +- nimble-angular README.md: This will document the `i18n` providers and the modules they're in, and the `withDefaults` directive which should be used at the root level of the page. It will also describe how the Nimble strings will now be included for translation after a `ng extract-i18n` run. +- nimble-blazor README.md: (Similar to the previous docs. This doc also needs to mention the theme provider, which it doesn't currently.) + ## Alternative Implementations / Designs **Alternative options for Nimble-provided labels:** @@ -294,4 +304,3 @@ The process above isn't great. When updated versions of Nimble Blazor are releas - Naming - Do we like having `i18n` in the element names, or should we pick something else? Once camel-cased it also looks strange, i.e. `NimbleI18nCore.razor` -- Best way to document this system (in Storybook)? From 81395bc04720582ab1259b82d6a34416f2c1f46f Mon Sep 17 00:00:00 2001 From: Malcolm Smith <20709258+msmithNI@users.noreply.github.com> Date: Tue, 13 Jun 2023 17:15:52 -0500 Subject: [PATCH 10/12] Note that we probably will create secondary entry points for the i18n providers, update some prototype links --- specs/labels-and-localization/README.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/specs/labels-and-localization/README.md b/specs/labels-and-localization/README.md index b49feb62fb..1259e0c4db 100644 --- a/specs/labels-and-localization/README.md +++ b/specs/labels-and-localization/README.md @@ -118,18 +118,16 @@ See the prototype branch: [localizable-labels-prototype-2](https://github.com/ni We'll define a base class (prototype: [i18n-base.ts](https://github.com/ni/nimble/blob/b13117639de55db3086561edccc4dfe5994f9829/packages/nimble-components/src/i18n/i18n-base.ts)) for the i18n providers, which handles setting the token values on the ancestor theme-provider. For each i18n provider, we'll have a file declaring the DesignTokens, with a class deriving from the base class that has attributes+properties for setting the token values (prototype: [i18n/core](https://github.com/ni/nimble/blob/b13117639de55db3086561edccc4dfe5994f9829/packages/nimble-components/src/i18n/core/index.ts) and [i18n/table](https://github.com/ni/nimble/blob/b13117639de55db3086561edccc4dfe5994f9829/packages/nimble-components/src/i18n/table/index.ts)). **nimble-angular** -Each i18n provider will have its own Angular directive and module (prototype: [nimble-i18n-core.directive](https://github.com/ni/nimble/blob/d51ee14dc49db7070e5cab726c225f69635de17b/angular-workspace/projects/ni/nimble-angular/src/directives/i18n/core/nimble-i18n-core.directive.ts) and [nimble-i18n-core.module](https://github.com/ni/nimble/blob/d51ee14dc49db7070e5cab726c225f69635de17b/angular-workspace/projects/ni/nimble-angular/src/directives/i18n/core/nimble-i18n-core.module.ts) for `i18n-core`.) +Each i18n provider will have its own Angular directive and module (prototype: [nimble-i18n-core.directive](https://github.com/ni/nimble/blob/cf6a2e1ae010d00dc7253c25658dd5a17b5f6215/angular-workspace/projects/ni/nimble-angular/i18n/core/nimble-i18n-core.directive.ts) and [nimble-i18n-core.module](https://github.com/ni/nimble/blob/cf6a2e1ae010d00dc7253c25658dd5a17b5f6215/angular-workspace/projects/ni/nimble-angular/i18n/core/nimble-i18n-core.module.ts) for `i18n-core`.) +We will probably also want to create secondary entry points in nimble-angular for each i18n provider. In order to make it easy/automatic for clients to pick up new localized strings/labels when they uptake new nimble-angular versions, each i18n provider has an additional directive that will set all of the Nimble-defined labels/strings, using Angular's `$localize` function on the English strings. -Prototype: [nimble-i18n-core-with-defaults.directive](https://github.com/ni/nimble/blob/d51ee14dc49db7070e5cab726c225f69635de17b/angular-workspace/projects/ni/nimble-angular/src/directives/i18n/core/nimble-i18n-core-with-defaults.directive.ts) +Prototype: [nimble-i18n-core-with-defaults.directive](https://github.com/ni/nimble/blob/cf6a2e1ae010d00dc7253c25658dd5a17b5f6215/angular-workspace/projects/ni/nimble-angular/i18n/core/nimble-i18n-core-with-defaults.directive.ts) If we define descriptions for each string, we can include it so it appears in the message files, such as: ``$localize`:Nimble number-field increment button label:Increment` ``. -Once an Angular app uptakes the nimble-angular version that introduces these i18n modules, running `ng extract-i18n` will result in the app pulling in Nimble-provided labels/strings for localization. (Prototype: [messages.xlf](https://github.com/ni/nimble/blob/d51ee14dc49db7070e5cab726c225f69635de17b/angular-workspace/projects/example-client-app/src/locales/messages.xlf), output of `ng extract-i18n` after upgrading to this nimble-angular version) -When they pull in new nimble-angular versions in the future and re-run that command, the new strings will again be pulled in for translation automatically. - For each i18n provider that an Angular app will use: -- The app imports that specific i18n module (prototype: [in example app module](https://github.com/ni/nimble/blob/d51ee14dc49db7070e5cab726c225f69635de17b/angular-workspace/projects/example-client-app/src/app/app.module.ts#L73)). -- The app adds that i18n element as a child to their theme provider ([prototype](https://github.com/ni/nimble/blob/d51ee14dc49db7070e5cab726c225f69635de17b/angular-workspace/projects/example-client-app/src/app/app.component.html#L2)): +- The app imports that specific i18n module (prototype: [in example app module](https://github.com/ni/nimble/blob/cf6a2e1ae010d00dc7253c25658dd5a17b5f6215/angular-workspace/projects/example-client-app/src/app/app.module.ts#L74)). +- The app adds that i18n element as a child to their theme provider ([prototype](https://github.com/ni/nimble/blob/cf6a2e1ae010d00dc7253c25658dd5a17b5f6215/angular-workspace/projects/example-client-app/src/app/app.component.html#L2)): ```html @@ -137,7 +135,10 @@ For each i18n provider that an Angular app will use: ``` - If the app needs to customize any of the labels, they can do so via the i18n directive API. Generally the root i18n provider would use `withDefaults` to set all the labels to their localized values, and any nested ones would not. -We expect most apps in SystemLink to consume both `nimble-i18n-core` and `nimble-i18n-table` as-is. +Once an Angular app uptakes the nimble-angular version that introduces these i18n modules, and references the i18n modules, running `ng extract-i18n` will result in the app pulling in Nimble-provided labels/strings for localization. (Prototype: [messages.xlf](https://github.com/ni/nimble/blob/d51ee14dc49db7070e5cab726c225f69635de17b/angular-workspace/projects/example-client-app/src/locales/messages.xlf), output of `ng extract-i18n`) +When they pull in new nimble-angular versions in the future and re-run that command, the new strings will again be pulled in for translation automatically. + +We expect most apps in SystemLink to consume both `nimble-i18n-core` and `nimble-i18n-table` as-is (in their app component HTML, as children of the `nimble-theme-provider`). We can consider codegen-ing the Angular directives, which would let us avoid copy-pasting the English strings/ descriptions at the nimble-angular level, but at the expense of obfucscating some of the code (in the generator scripts). From 2658ee72e41c4cd778fd03e812d72a8c49526694 Mon Sep 17 00:00:00 2001 From: Malcolm Smith <20709258+msmithNI@users.noreply.github.com> Date: Tue, 13 Jun 2023 17:23:12 -0500 Subject: [PATCH 11/12] Update nimble-i18n-* to nimble-label-provider-*, other HLD feedback --- specs/labels-and-localization/README.md | 74 ++++++++++++------------- 1 file changed, 35 insertions(+), 39 deletions(-) diff --git a/specs/labels-and-localization/README.md b/specs/labels-and-localization/README.md index 1259e0c4db..074d223c84 100644 --- a/specs/labels-and-localization/README.md +++ b/specs/labels-and-localization/README.md @@ -38,66 +38,66 @@ Our general approach to localization is that clients will handle localizing stri Conceptually the localized strings are very similar to the sharing pattern of Design Tokens. The vast majority of the time, you want to use the same value (i.e. numeric increment/decrement button text is not control instance specific). However, we do want to be able to override the strings for specific controls as needed. We will create non-CSS-property design tokens, similar to the `theme` and `direction` design tokens. -We will also create new `nimble-i18n-*` elements with APIs for setting the tokens. These will generally be direct children of the `nimble-theme-provider`, and the theme provider will be the target that the token values are set on. +We will also create new `nimble-label-provider-*` elements with APIs for setting the tokens. These will generally be direct children of the `nimble-theme-provider`, and the theme provider will be the target that the token values are set on. ```html - - + + > ``` Pros: - Can configure once for the whole page in one spot, does not need to be per control. Much fewer duplicated attributes / slotted elements in the page. -- Can override for specific elements as needed (by wrapping in `nimble-theme-provider` and `nimble-i18n-*` elements) -- We can make more granular i18n providers, even down to the level of individual controls, if we're concerned about the number of strings apps would be automatically pulling in. (Note that currently we're just planning to have 2 though.) -- Code outside of Nimble could use the same concept (and derive from our i18n base class). If we end up with new controls in Nimble that are mostly specific to specific apps, we can also create new i18n providers for those elements, rather than sticking them all in the `i18n-core` element. +- Can override for specific elements as needed (by wrapping in `nimble-theme-provider` and `nimble-label-provider-*` elements) +- We can make more granular label-providers, even down to the level of individual controls, if we're concerned about the number of strings apps would be automatically pulling in. (Note that currently we're just planning to have 2 though.) +- Code outside of Nimble could use the same concept (and derive from our label-provider base class). If we end up with new controls in Nimble that are mostly specific to specific apps, we can also create new label-providers for those elements, rather than sticking them all in the `label-provider-core` element. Cons - More verbose than putting the APIs directly on nimble-theme-provider (but that's not really extensible) - Still somewhat verbose in the page, to have all of the localized strings as attributes - We can consider using the `fromView` attribute mode on the attributes for the strings on the theme-provider. If clients set the strings using the theme-provider properties rather than as attributes in the HTML (as is currently proposed for Angular), `fromView` would not reflect those strings back to the DOM. -- Apps may be pulling in more strings than they need (i.e. if they just use the banner, they'll pick up the rest of the strings in `i18n-core`). Mitigation is to split off large sets of strings (e.g. those for the table). +- Apps may be pulling in more strings than they need (i.e. if they just use the banner, they'll pick up the rest of the strings in `label-provider-core`). Mitigation is to split off large sets of strings (e.g. those for the table). - Not an originally intended use case for DesignToken Other notes: - We don't expect to need mixed content (i.e. other than simple strings), so attributes should be sufficient (vs. slots) -- If the page was automatically translated by something like Google Translate, the attributes on the i18n providers don't get translated. However we think this is OK because the expected usages of the labels (button content, slotted content, etc) would get translated. +- If the page was automatically translated by something like Google Translate, the attributes on the label-providers don't get translated. However we think this is OK because the expected usages of the labels (button content, slotted content, etc) would get translated. - We may want to provide a description along with each English string, to aid in translation. The current set of known labels for Nimble is shown below: -**nimble-i18n-core** +**nimble-label-provider-core** | Token Name | English string | |------------------------|----------------| | banner-dismiss | Close | | number-field-increment | Increment | | number-field-decrement | Decrement | -**nimble-i18n-table** +**nimble-label-provider-table** | Token Name | English string | |---------------------------------------|---------------------| | table-group-collapse | Collapse group | | table-group-expand | Expand group | | table-groups-collapse-all | Collapse all groups | | table-cell-action-menu-label | Options | -| table-column-header-menu | Column Options | +| table-column-header-menu | Column options | | table-column-header-grouped-indicator | Grouped | | table-column-sort-ascending | Sort ascending | | table-column-sort-descending | Sort descending | | table-column-group-by | Group by | | table-column-size-to-content | Size to content | -Note: We will probably remove the `table` prefix from the properties and attribute names on `nimble-i18n-table` as it's redundant. `table-cell-action-menu-label` is a fallback for when column.actionMenuLabel is unset. +Note: We will probably remove the `table` prefix from the properties and attribute names on `nimble-label-provider-table` as it's redundant. `table-cell-action-menu-label` is a fallback for when column.actionMenuLabel is unset. The expected format for token names is: - component name, e.g. `number-field` or `table` - component part/category (optional), e.g. `column-header` - specific functionality or sub-part, e.g. `decrement` -- the suffix `label` (will be omitted from the i18n properties/attributes) +- the suffix `label` (will be omitted from the label-provider properties/attributes) Example: ```ts @@ -105,49 +105,49 @@ export const numberFieldIncrementLabel = DesignToken.create({ name: 'number-field-increment-label', cssCustomPropertyName: null }).withDefault('Increment'); -// on the i18n element: +// on the label-provider element: @attr({ attribute: 'number-field-increment' }) public numberFieldIncrement = 'Increment'; ``` #### Implementation Details -See the prototype branch: [localizable-labels-prototype-2](https://github.com/ni/nimble/compare/%40ni/nimble-angular_v16.6.3...localizable-labels-prototype-2?expand=1) +See the prototype branch: [localizable-labels-prototype-2](https://github.com/ni/nimble/compare/%40ni/nimble-angular_v16.6.3...localizable-labels-prototype-2?expand=1), but note the prototype used the name `i18n` instead of the current proposal `label-provider`. **nimble-components** -We'll define a base class (prototype: [i18n-base.ts](https://github.com/ni/nimble/blob/b13117639de55db3086561edccc4dfe5994f9829/packages/nimble-components/src/i18n/i18n-base.ts)) for the i18n providers, which handles setting the token values on the ancestor theme-provider. For each i18n provider, we'll have a file declaring the DesignTokens, with a class deriving from the base class that has attributes+properties for setting the token values (prototype: [i18n/core](https://github.com/ni/nimble/blob/b13117639de55db3086561edccc4dfe5994f9829/packages/nimble-components/src/i18n/core/index.ts) and [i18n/table](https://github.com/ni/nimble/blob/b13117639de55db3086561edccc4dfe5994f9829/packages/nimble-components/src/i18n/table/index.ts)). +We'll define a base class (prototype: [i18n-base.ts](https://github.com/ni/nimble/blob/b13117639de55db3086561edccc4dfe5994f9829/packages/nimble-components/src/i18n/i18n-base.ts)) for the label-providers, which handles setting the token values on the ancestor theme-provider. For each label-provider, we'll have a file declaring the DesignTokens, with a class deriving from the base class that has attributes+properties for setting the token values (prototype: [i18n/core](https://github.com/ni/nimble/blob/b13117639de55db3086561edccc4dfe5994f9829/packages/nimble-components/src/i18n/core/index.ts) and [i18n/table](https://github.com/ni/nimble/blob/b13117639de55db3086561edccc4dfe5994f9829/packages/nimble-components/src/i18n/table/index.ts)). **nimble-angular** -Each i18n provider will have its own Angular directive and module (prototype: [nimble-i18n-core.directive](https://github.com/ni/nimble/blob/cf6a2e1ae010d00dc7253c25658dd5a17b5f6215/angular-workspace/projects/ni/nimble-angular/i18n/core/nimble-i18n-core.directive.ts) and [nimble-i18n-core.module](https://github.com/ni/nimble/blob/cf6a2e1ae010d00dc7253c25658dd5a17b5f6215/angular-workspace/projects/ni/nimble-angular/i18n/core/nimble-i18n-core.module.ts) for `i18n-core`.) -We will probably also want to create secondary entry points in nimble-angular for each i18n provider. +Each label-provider will have its own Angular directive and module (prototype: [nimble-i18n-core.directive](https://github.com/ni/nimble/blob/cf6a2e1ae010d00dc7253c25658dd5a17b5f6215/angular-workspace/projects/ni/nimble-angular/i18n/core/nimble-i18n-core.directive.ts) and [nimble-i18n-core.module](https://github.com/ni/nimble/blob/cf6a2e1ae010d00dc7253c25658dd5a17b5f6215/angular-workspace/projects/ni/nimble-angular/i18n/core/nimble-i18n-core.module.ts) for `label-provider-core`.) +We will probably also want to create secondary entry points in nimble-angular for each label-provider, which ensures that client apps won't necessary pull in all the Nimble labels from all label providers (unless they import them explicitly). -In order to make it easy/automatic for clients to pick up new localized strings/labels when they uptake new nimble-angular versions, each i18n provider has an additional directive that will set all of the Nimble-defined labels/strings, using Angular's `$localize` function on the English strings. +In order to make it easy/automatic for clients to pick up new localized strings/labels when they uptake new nimble-angular versions, each label-provider has an additional directive that will set all of the Nimble-defined labels/strings, using Angular's `$localize` function on the English strings. Prototype: [nimble-i18n-core-with-defaults.directive](https://github.com/ni/nimble/blob/cf6a2e1ae010d00dc7253c25658dd5a17b5f6215/angular-workspace/projects/ni/nimble-angular/i18n/core/nimble-i18n-core-with-defaults.directive.ts) If we define descriptions for each string, we can include it so it appears in the message files, such as: ``$localize`:Nimble number-field increment button label:Increment` ``. -For each i18n provider that an Angular app will use: -- The app imports that specific i18n module (prototype: [in example app module](https://github.com/ni/nimble/blob/cf6a2e1ae010d00dc7253c25658dd5a17b5f6215/angular-workspace/projects/example-client-app/src/app/app.module.ts#L74)). -- The app adds that i18n element as a child to their theme provider ([prototype](https://github.com/ni/nimble/blob/cf6a2e1ae010d00dc7253c25658dd5a17b5f6215/angular-workspace/projects/example-client-app/src/app/app.component.html#L2)): +For each label-provider that an Angular app will use: +- The app imports that specific label-provider module (prototype: [in example app module](https://github.com/ni/nimble/blob/cf6a2e1ae010d00dc7253c25658dd5a17b5f6215/angular-workspace/projects/example-client-app/src/app/app.module.ts#L74)). +- The app adds that label-provider element as a child to their theme provider ([prototype](https://github.com/ni/nimble/blob/cf6a2e1ae010d00dc7253c25658dd5a17b5f6215/angular-workspace/projects/example-client-app/src/app/app.component.html#L2)): ```html - + ``` -- If the app needs to customize any of the labels, they can do so via the i18n directive API. Generally the root i18n provider would use `withDefaults` to set all the labels to their localized values, and any nested ones would not. +- If the app needs to customize any of the labels, they can do so via the label-provider directive API. Generally the root label-provider would use `withDefaults` to set all the labels to their localized values, and any nested ones would not. -Once an Angular app uptakes the nimble-angular version that introduces these i18n modules, and references the i18n modules, running `ng extract-i18n` will result in the app pulling in Nimble-provided labels/strings for localization. (Prototype: [messages.xlf](https://github.com/ni/nimble/blob/d51ee14dc49db7070e5cab726c225f69635de17b/angular-workspace/projects/example-client-app/src/locales/messages.xlf), output of `ng extract-i18n`) +Once an Angular app uptakes the nimble-angular version that introduces these label-provider modules, and references the label-provider modules, running `ng extract-i18n` will result in the app pulling in Nimble-provided labels/strings for localization. (Prototype: [messages.xlf](https://github.com/ni/nimble/blob/d51ee14dc49db7070e5cab726c225f69635de17b/angular-workspace/projects/example-client-app/src/locales/messages.xlf), output of `ng extract-i18n`) When they pull in new nimble-angular versions in the future and re-run that command, the new strings will again be pulled in for translation automatically. -We expect most apps in SystemLink to consume both `nimble-i18n-core` and `nimble-i18n-table` as-is (in their app component HTML, as children of the `nimble-theme-provider`). +We expect most apps in SystemLink to consume both `nimble-label-provider-core` and `nimble-label-provider-table` as-is (in their app component HTML, as children of the `nimble-theme-provider`). We can consider codegen-ing the Angular directives, which would let us avoid copy-pasting the English strings/ descriptions at the nimble-angular level, but at the expense of obfucscating some of the code (in the generator scripts). **nimble-blazor** We currently don't have a good solution for Blazor clients to automatically pick up or localize our labels/strings. -We do still plan to create Razor components for each i18n provider, so that Blazor clients can manually specify/localize the labels if desired. -(Prototype: [NimbleI18nCore.razor](https://github.com/ni/nimble/compare/@ni/nimble-angular_v16.6.3...localizable-labels-prototype-2?expand=1#diff-88863ebb8b90aab301573eeb66b6850c26327d12be6b0fa33bcd3cccaadca938) for `i18n-core`). +We do still plan to create Razor components for each label-provider, so that Blazor clients can manually specify/localize the labels if desired. +(Prototype: [NimbleI18nCore.razor](https://github.com/ni/nimble/compare/@ni/nimble-angular_v16.6.3...localizable-labels-prototype-2?expand=1#diff-88863ebb8b90aab301573eeb66b6850c26327d12be6b0fa33bcd3cccaadca938) for `label-provider-core`). -If we have any clients that will be using Nimble Blazor and non-English locales, we should probably do additional research to see if we can come up with a more seamless approach. Note that the `i18n-core` labels are not visible / are for accessibility only, so this may only be a priority for clients using the Nimble table (which will have visible strings needing localization). +If we have any clients that will be using Nimble Blazor and non-English locales, we should probably do additional research to see if we can come up with a more seamless approach. Note that the `label-provider-core` labels are not visible / are for accessibility only, so this may only be a priority for clients using the Nimble table (which will have visible strings needing localization). ### Plan for Client-Provided Labels Examples: Button content, menu item content @@ -164,10 +164,10 @@ The banner's `dismissButtonLabel` will be redundant once we have a Nimble-provid We'll need to update our documentation to describe this new system: - Storybook - - Per-component: If a component uses localizable labels, its Storybook docs should list the label names, and which `i18n` provider they're a part of. - - (Optional) Consider adding a new top-level page like `Concepts/Localization` which describes the `i18n` providers. We could also consider a single page that describes `nimble-theme-provider`, plus the `nimble-i18n-*` providers. As an alternative, we could make a `Strings/Labels` page under `Tokens` with sections for each `i18n` provider and the label names they contain, but that may be somewhat redundant if each component lists the label names too. -- nimble-components README.md: This will document the `i18n` providers and where they should go on the page, similiar to the current theme-provider documentation. -- nimble-angular README.md: This will document the `i18n` providers and the modules they're in, and the `withDefaults` directive which should be used at the root level of the page. It will also describe how the Nimble strings will now be included for translation after a `ng extract-i18n` run. + - Per-component: If a component uses localizable labels, its Storybook docs should list the label names, and which `label-provider` they're a part of. + - (Optional) Consider adding a new top-level page like `Concepts/Localization` which describes the `label-provider`s. We could also consider a single page that describes `nimble-theme-provider`, plus the `nimble-label-provider-*` elements. As an alternative, we could make a `Strings/Labels` page under `Tokens` with sections for each `label-provider` and the label names they contain, but that may be somewhat redundant if each component lists the label names too. +- nimble-components README.md: This will document the `label-provider`s and where they should go on the page, similiar to the current theme-provider documentation. +- nimble-angular README.md: This will document the `label-providers` and the modules they're in, and the `withDefaults` directive which should be used at the root level of the page. It will also describe how the Nimble strings will now be included for translation after a `ng extract-i18n` run. - nimble-blazor README.md: (Similar to the previous docs. This doc also needs to mention the theme provider, which it doesn't currently.) ## Alternative Implementations / Designs @@ -301,7 +301,3 @@ The process above isn't great. When updated versions of Nimble Blazor are releas - Add an `IStringLocalizer` property to the `NimbleThemeProvider` Razor component, and codegen the `Label*` properties to use it if it's set. This would improve the process of telling Nimble about the localized resources, but wouldn't change the process of manually copying the resx to start with. - Define an MSBuild task that copies the resx file, similar to what's outlined in [this GitHub comment](https://github.com/ni/nimble/issues/558#issuecomment-1129279985). Not much better / still a manual process. -## Open Issues - -- Naming - - Do we like having `i18n` in the element names, or should we pick something else? Once camel-cased it also looks strange, i.e. `NimbleI18nCore.razor` From 497dd1e5157ca4322722f204c6f884e6f5dba2ef Mon Sep 17 00:00:00 2001 From: Malcolm Smith <20709258+msmithNI@users.noreply.github.com> Date: Tue, 13 Jun 2023 17:28:42 -0500 Subject: [PATCH 12/12] Update naming guidance to better match our existing token naming guidance --- specs/labels-and-localization/README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/specs/labels-and-localization/README.md b/specs/labels-and-localization/README.md index 074d223c84..f493c264df 100644 --- a/specs/labels-and-localization/README.md +++ b/specs/labels-and-localization/README.md @@ -73,9 +73,9 @@ The current set of known labels for Nimble is shown below: **nimble-label-provider-core** | Token Name | English string | |------------------------|----------------| -| banner-dismiss | Close | -| number-field-increment | Increment | -| number-field-decrement | Decrement | +| banner-dismiss | Close | +| number-field-increment | Increment | +| number-field-decrement | Decrement | **nimble-label-provider-table** | Token Name | English string | @@ -94,7 +94,8 @@ The current set of known labels for Nimble is shown below: Note: We will probably remove the `table` prefix from the properties and attribute names on `nimble-label-provider-table` as it's redundant. `table-cell-action-menu-label` is a fallback for when column.actionMenuLabel is unset. The expected format for token names is: -- component name, e.g. `number-field` or `table` +- element/type(s) to which the token applies, e.g. `number-field` or `table` + - This may not be an exact element name, if this label applies to multiple elements or will be used in multiple contexts - component part/category (optional), e.g. `column-header` - specific functionality or sub-part, e.g. `decrement` - the suffix `label` (will be omitted from the label-provider properties/attributes)