Skip to content

Commit

Permalink
feat(l10n): add graphql transformers and localize (#1885)
Browse files Browse the repository at this point in the history
* feat(l10n): add localize

* chore: add lodash

* docs(l10n): add docs

* refactor(l10n/localize): allow consumer to specify Output

* refactor: transformLocalizedFieldToString -> transformLocalizedFieldToLocalizedString

* docs(l10n): rename fieldTransformDefinitions -> fieldNameTransformationMappings

* docs(l10n): rephrase, useMcQuery, rename fetchedProduct -> product

* refactor(l10n): rename injectTransformedLocalizedFields -> applyTransformedLocalizedFields

* refactor(l10n): do not expose transformLocalizedFieldToLocalizedString,

* chore: add changeset

* refactor(l10n): move types to `./types`

* feat(l10n): add `localize`

* refactor(localize): rename `language` -> locale

* refactor(localize): simplify mapping for `namesToOmit`

* chore(l10n/readme): simplify locale mapping

* chore: rename addFallbackHint -> formatLocalizedFallbackHint

* chore: update signature docs

* docs(l10n): correct phrasing

* docs(localize): update README

Co-authored-by: Nicola Molinari <[email protected]>

* chore(l10n/readme): update according review suggestions

Co-authored-by: Nicola Molinari <[email protected]>

* refactor(l10n): update signature, localize -> formatLocalizedString in README

* refactor(l10n): rename LocalizeSignatureOptions -> FormatLocalizedStringOptions

* docs(l10n/readme): reword

Co-authored-by: Tobias Deekens <[email protected]>

* refactor(l10n/types): use `Record`

* docs(l10n/readme): remove wording about "use with caution"

Co-authored-by: Nicola Molinari <[email protected]>
Co-authored-by: Tobias Deekens <[email protected]>
  • Loading branch information
3 people authored Nov 25, 2020
1 parent 58a2035 commit a8e2202
Show file tree
Hide file tree
Showing 9 changed files with 1,238 additions and 687 deletions.
5 changes: 5 additions & 0 deletions .changeset/young-months-sing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@commercetools-frontend/l10n': minor
---

feature: add `applyTransformedLocalizedFields` to `l10n`
261 changes: 261 additions & 0 deletions packages/l10n/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,264 @@ First, download the data (`core.zip`) for a specific version from the [downloads
Then extract the data and copy the `core` folder to this package, and rename it to e.g. `cldr-v35`. Then point the `cldr` library to the folder location.

Run the script, which uses the new data.

## Utils

### `applyTransformedLocalizedFields`

> Transforms multiple `LocalizedField` -> `LocalizedString`, given `fieldNameTransformationMappings`
#### Context

Amongst [Common Types](https://docs.commercetools.com/api/types) in the commercetools platform API, we find [`LocalizedString`](https://docs.commercetools.com/api/types#localizedstring) type.

The `LocalizedString` is a type reserved for values found in a [Resource](https://docs.commercetools.com/api/types#referencetype), for example `Product`:

```js
// Product
{
// `name` is a `LocalizedString`
// as defined by https://docs.commercetools.com/api/projects/products#productdata
name: {
en: 'Milk';
}
}
```

The documented `LocalizedString` in [https://docs.commercetools.com](https://docs.commercetools.com/) is a specification of the HTTP API.

However, the commercetools platform `/graphql` API represents the `LocalizedString` as a list of the same name.

```js
// Product, returned from the `/graphql` API of commercetools platform
{
nameAllLocales: [
{
locale: 'en',
value: 'Milk',
},
];
}
```

To distinguish these in source code of the Merchant Center, we name the graphql shaped value `LocalizedField`. We offer `applyTransformedLocalizedFields` that is authored to transform these values from one to the other.

#### Example usage

```js
const product = {
nameAllLocales: [
{
locale: 'en',
value: 'Milk',
},
],
descriptionAllLocales: [
{
locale: 'en',
value: 'This is milk',
},
],
};
const fieldNameTransformationMappings = [
{
from: 'nameAllLocales',
to: 'name',
},
{
from: 'descriptionAllLocales',
to: 'description',
},
];
const transformedProduct = applyTransformedLocalizedFields(
fetchedProduct,
fieldNameTransformationMappings
);

console.log(transformedProduct);
// { name: { en: 'Milk' }, description: { en: 'This is milk' } }
```

#### When to use it

This transformation tool will be helpful when you consume the commercetools platform `/graphql` API in conjunction with authoring views consuming `@commercetools-frontend/ui-kit`.

Given that you consume:

- The commercetools platform `/graphql` API
- [`LocalizedTextInput`](https://github.com/commercetools/ui-kit/blob/master/packages/components/inputs/localized-text-input/src/localized-text-input.js)

This will be helpful transforming data from `response -> view`.

```js
// fetching product from the commercetools platform `/graphql` API
// returns a product with a `nameAllLocales` and `descriptionAllLocales`
const product = useMcQuery(ProductQuery, {
context: GRAPHQL_TARGETS.COMMERCETOOLS_PLATFORM,
});

// Given that LocalizedTextInput accepts a value of `{ [key: string]: string }`,
// we tranform our product to match the required shape
const transformedProduct = applyTransformedLocalizedFields(
product,
fieldNameTransformMappings
);

// Finally, we are ready to render our form with the correctly shaped `name` and `description`
return (
<>
<LocalizedTextInput name="name" value={transformedProduct.name} />
<LocalizedTextInput
name="description"
value={transformedProduct.description}
/>
</>
);
```

### `formatLocalizedString` (DRAFT)

> Transforms a `LocalizedString` to a String
The `formatLocalizedString` util is a util we use internally inside the Merchant Center. This util is at the moment subject to change, hence marked as `Draft`.

#### Context

As discussed under `applyTransformedLocalizedFields`, a `LocalizedString` is a value type reserved for a `Resource`, e.g a `Product`.

```js
const product = {
name: {
en: 'Milk',
de: 'Milch',
},
};
```

When rendering a Product in a view, we would like to render the value of a localized string field, such as `name`, given the selected **data locale** of the UI.
The **data locale** is a value controlled by the Merchant Center User (MC User) by changing the **data locale switcher** in the top bar of the Merchant Center. The list of available options is derived by the list of languages specified in the project. The selected value can be read from the **application context**.

However, there might be a case where the selected **data locale** does not match any of the localized string values. In this case, it is recommended to display a "fallback" value using the `formatLocalizedString` function.

The `formatLocalizedString` util is authored with the following in mind:

1. Help us Custom Application developers remain resilient as we attempt to derive a value of `LocalizedString` given a specified locale.
2. Help us Custom Application developers remain consistent when rendering a value derived from `LocalizedString`.

Let us take a look at the examples below putting this to action.

#### Example usage

All examples below will use the following `Product`:

```
const product = {
name: {
en: 'Milk',
de: 'Milch,
}
}
```

##### Scenario 1

Given that selected **data locale** of `de`

```js
const translatedName = formatLocalizedString(product, {
key: 'name',
locale: 'de',
});

console.log(translatedName);
// 'Milch'
```

##### Scenario 2

Given that selected **data locale** of `sv`

```js
const translatedName = formatLocalizedString(product, {
key: 'name',
locale: 'sv',
});

console.log(translatedName);
// 'Milk (EN)'
```

**What happened?**

Our product has no value for the selected **data locale** `sv`. The `formatLocalizedString` function selects the "next available value" (more details on the order below) from `product.name`. In this case it's `en`, and returns it with a hint `(EN)` that this value refers to another locale.

##### Scenario 3

Given that selected **data locale** of `de-AT`

```js
const translatedName = formatLocalizedString(product, {
key: 'name',
locale: 'de-AT',
});

console.log(translatedName);
// 'Milch'
```

**What happened?**

Our product has no matching value for the selected **data locale** `de-AT` but we still got `"Milch"`.
This is because `formatLocalizedString` attempts to determine a **primary language tag** on the option `locale` and match that to the available values. Given that `de` is a [primary](https://en.wikipedia.org/wiki/IETF_language_tag) of `de-AT`, it determines that this is the _closest available value_ from `product.name`

#### Fallback

To provide even more freedom beyond cases mentioned above the `formatLocalizedString` allows specifying a `fallback` as a last resort:

```js
const translatedName = formatLocalizedString(product, {
obj: product,
key: 'name',
locale: 'sv',
fallback: '-',
});
```

The default value of `fallback` is a `""`.

#### Fallback order

In **Scenario 2** above, we discussed that `formatLocalizedString` will pick the "next available value" from `product.name` when there is no matching value. In our case, the next available value was the locale `en`.
Custom Application developers can take full control over the order of attempted lookups of the value while there is no match. Before `formatLocalizedString` eventually proceeds to rendering what is specified as `fallback`.

`formatLocalizedString` accepts `fallbackOrder`, and this [test exemplifies the use case and resolve](./src/localize.spec.ts#L151:L170).

#### When to use it

Given that we want to render `LocalizedString` of a given `Resource`, it is sensible to rely on `formatLocalizedString` in conjunction with the Application Context. A user usually has a defined preference of languages we can use.

```js
import Text from '@commercetools-uikit/text';
import { useApplicationContext } from '@commercetools-frontend/application-shell-connectors';

const { dataLocale, projectLanguages } = useApplicationContext(
(applicationContext) => ({
dataLocale: applicationContext.dataLocale,
// The Application Context also exposes the languages that are defined on the Project settings
// we can rely on this to determine the fallback order.
// This helps with consistency, although you can specify the fallback order however you want
projectLanguages: context.project.languages,
})
);

return (
<Text.Headline>
{formatLocalizedString(product, {
key: 'name',
locale: dataLocale,
fallback: '<MY_CUSTOM_FALLBACK>',
fallbackOrder: projectLanguages,
})}
</Text.Headline>
);
```
2 changes: 2 additions & 0 deletions packages/l10n/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
"@babel/runtime-corejs3": "7.12.5",
"@commercetools-frontend/sentry": "17.4.1",
"@types/prop-types": "^15.7.3",
"lodash": "4.17.20",
"lodash-es": "4.17.15",
"moment": "^2.24.0",
"prop-types": "15.7.2"
},
Expand Down
5 changes: 5 additions & 0 deletions packages/l10n/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,8 @@ export {
withTimeZones,
timeZonesShape,
} from './time-zone-information';

export {
applyTransformedLocalizedFields,
formatLocalizedString,
} from './localize';
Loading

1 comment on commit a8e2202

@vercel
Copy link

@vercel vercel bot commented on a8e2202 Nov 25, 2020

Choose a reason for hiding this comment

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

Please sign in to comment.