Skip to content

Commit

Permalink
feat(foxy-i18n): add simplify-ns-loading attribute to load only top…
Browse files Browse the repository at this point in the history
…-level namespaces
  • Loading branch information
pheekus committed Mar 28, 2024
1 parent e4fd9cd commit 751df4f
Show file tree
Hide file tree
Showing 12 changed files with 1,440 additions and 144 deletions.
1,455 changes: 1,333 additions & 122 deletions custom-elements.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const AddingPagesGuideOrdersDemo = (): TemplateResult => html`
<foxy-customer-portal
base="https://demo.api/portal/"
hiddencontrols="customer:header:actions:edit customer:subscriptions customer:transactions customer:addresses customer:payment-methods"
simplify-ns-loading
>
<template slot="customer:header:before">
<style>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const getHref = (page: string) => {
};

export const AddingPagesGuideProfileDemo = (): TemplateResult => html`
<foxy-customer-portal base="https://demo.api/portal/">
<foxy-customer-portal base="https://demo.api/portal/" simplify-ns-loading>
<template slot="customer:header:before">
<style>
a {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ class PromoElement extends LitElement {
customElements.define('demo-promo', PromoElement);

export const DynamicContentGuideDemo = (): TemplateResult => html`
<foxy-customer-portal base="https://demo.api/portal/">
<foxy-customer-portal base="https://demo.api/portal/" simplify-ns-loading>
<template slot="customer:header:after">
<demo-promo email="\${host.data.email}" style="margin: var(--lumo-space-m) 0"></demo-promo>
</template>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ import '../index.ts';
To get started, make sure you've got your basic setup ready on the main page – in our guide that'd be `index.html`. If you're building a single page application, you'll need to include the JS portion only once.

```html
<foxy-customer-portal base="https://foxy-demo.foxycart.com/s/customer/"></foxy-customer-portal>
<foxy-customer-portal
base="https://foxy-demo.foxycart.com/s/customer/"
simplify-ns-loading
></foxy-customer-portal>

<script type="module">
import 'https://unpkg.com/@foxy.io/elements@1/dist/cdn/foxy-customer-portal.js';
Expand All @@ -38,7 +41,7 @@ To get started, make sure you've got your basic setup ready on the main page –
Let's add a `<nav>` element with `<a>` links. We can't use slots since their content is rendered for everyone regardless of auth status, but we can use a special feature that comes with every configurable element from Foxy – **templates**. Just like slots, templates allow developers to inject custom markup in predefined positions, except that it won't be rendered if template host is hidden. Let's add our navigation right **before the header**:

```html
<foxy-customer-portal base="https://foxy-demo.foxycart.com/s/customer/">
<foxy-customer-portal base="https://foxy-demo.foxycart.com/s/customer/" simplify-ns-loading>
<template slot="customer:header:before">
<nav>
<a href="./index.html">My Profile</a>
Expand All @@ -56,7 +59,7 @@ If you run this code, you'll need to log in to see the links. It works!
Let's add some CSS to make it look a bit better. Every template is rendered in an isolated Shadow DOM, so you'll need to link or define your styles **inside of the template**. Quick tip: use [Lumo](https://demo.vaadin.com/lumo-editor/) to make your custom content look native to the portal.

```html
<foxy-customer-portal base="https://foxy-demo.foxycart.com/s/customer/">
<foxy-customer-portal base="https://foxy-demo.foxycart.com/s/customer/" simplify-ns-loading>
<template slot="customer:header:before">
<style>
a {
Expand Down Expand Up @@ -86,6 +89,7 @@ For our orders page, let's copy the contents of `index.html` and paste them into
<foxy-customer-portal
hiddencontrols="customer:header:actions:edit customer:subscriptions customer:transactions customer:addresses customer:payment-methods"
base="https://foxy-demo.foxycart.com/s/customer/"
simplify-ns-loading
>
<template slot="customer:header:before">...</template>
</foxy-customer-portal>
Expand All @@ -97,6 +101,7 @@ That's our custom page. To fill it with content, we can **use the `customer:defa
<foxy-customer-portal
hiddencontrols="customer:header:actions:edit customer:subscriptions customer:transactions customer:addresses customer:payment-methods"
base="https://foxy-demo.foxycart.com/s/customer/"
simplify-ns-loading
>
<template slot="customer:header:before">...</template>
<template slot="customer:default">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ Parent elements pass templates to their children and keep them updated, allowing
To get started, make sure you've got your basic setup ready. We'll need to write a bit of JS in this guide, so feel free to also create a dedicated file for your code – but we'll be using an inline script for simplicity.

```html
<foxy-customer-portal base="https://foxy-demo.foxycart.com/s/customer/"></foxy-customer-portal>
<foxy-customer-portal
base="https://foxy-demo.foxycart.com/s/customer/"
simplify-ns-loading
></foxy-customer-portal>

<script type="module">
import 'https://unpkg.com/@foxy.io/elements@1/dist/cdn/foxy-customer-portal.js';
Expand All @@ -44,7 +47,7 @@ There are two ways to define templates: in HTML (for simple embeds) and using JS
HTML templates are defined with the `<template>` tag and identified by the `slot` attribute. Template content is parsed as a regular lit-html template, which means it supports dynamic data binding with the familiar JS literal syntax. **Warning: templates can be vulnerable to XSS if used with unsafe data sources. If you're in doubt, consider using JS templates instead.**

```html
<foxy-customer-portal base="https://foxy-demo.foxycart.com/s/customer/">
<foxy-customer-portal base="https://foxy-demo.foxycart.com/s/customer/" simplify-ns-loading>
<template slot="customer:header:after">
Welcome back, ${host.data.first_name}! This content is rendered with an HTML template.
</template>
Expand Down Expand Up @@ -76,7 +79,7 @@ For this guide we'll use [Lit](https://lit.dev) to create a web component loadin
Let's extend our default setup with an HTML template rendering `<demo-promo>` element, and a corresponding custom element definition registering `PromoElement`:
```html
<foxy-customer-portal base="https://foxy-demo.foxycart.com/s/customer/">
<foxy-customer-portal base="https://foxy-demo.foxycart.com/s/customer/" simplify-ns-loading>
<template slot="customer:header:after">
<demo-promo></demo-promo>
</template>
Expand Down Expand Up @@ -169,7 +172,7 @@ Now that we've got the UI sorted out, it's time to make it dynamic. For the purp
Attributes are a standard way of passing data to the elements and they work perfectly with HTML templates. Let's add an attribute named `email`, link it to a property with the same name in `<demo-promo>` and bind it to host data:
```html
<foxy-customer-portal base="https://foxy-demo.foxycart.com/s/customer/">
<foxy-customer-portal base="https://foxy-demo.foxycart.com/s/customer/" simplify-ns-loading>
<template slot="customer:header:after">
<demo-promo email="${host.data.email}"></demo-promo>
</template>
Expand Down
50 changes: 44 additions & 6 deletions src/elements/public/I18n/I18n.stories.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,14 @@ addEventListener('fetch', (event: Event) => {

Whenever I18n element needs to load translations for a language or a namespace, it dispatches the `fetch` event on `window`, requesting a resource at a special URL formatted as `foxy://i18n/NAMESPACE/LANGUAGE` (e.g. `foxy://i18n/shared/en`). We can then rewrite that URL, use mock data or get translations from a local cache if we like. Once loaded, i18next will make those translations avaialable to every `<foxy-i18n>` element on the page. Let's render something from the shared i18n file that comes with Foxy Elements:

<Canvas mdxSource='<foxy-i18n key="first_name"></foxy-i18n>' isExpanded>
<Story name="With key">{() => html`<foxy-i18n key="first_name"></foxy-i18n>`}</Story>
<Canvas mdxSource='<foxy-i18n key="demo_key"></foxy-i18n>' isExpanded>
<Story name="With key">{() => html`<foxy-i18n key="demo_key"></foxy-i18n>`}</Story>
</Canvas>

Cool! Let's see what it looks like in Spanish by setting the [lang attribute](https://developer.mozilla.org/en/docs/Web/HTML/Global_attributes/lang):

<Canvas mdxSource='<foxy-i18n key="first_name" lang="es"></foxy-i18n>' isExpanded>
<Story name="With lang">{() => html`<foxy-i18n key="first_name" lang="es"></foxy-i18n>`}</Story>
<Canvas mdxSource='<foxy-i18n key="demo_key" lang="es"></foxy-i18n>' isExpanded>
<Story name="With lang">{() => html`<foxy-i18n key="demo_key" lang="es"></foxy-i18n>`}</Story>
</Canvas>

How about some country names? We don't always need them, so placing them in the main translation file is a bit uneffective. A separate namespace works much better for such use cases:
Expand All @@ -73,14 +73,52 @@ How about some country names? We don't always need them, so placing them in the
<Story name="With ns">{() => html`<foxy-i18n key="GB" ns="country"></foxy-i18n>`}</Story>
</Canvas>

You can specify paths to get nested values. In the example below, `foxy-i18n` will load a corresponding file for the `demo` namespace and will look for a nested value at this path: `one.two.three`.

```html
<foxy-i18n key="one.two.three" ns="demo"></foxy-i18n>
```

You can also specify multiple namespaces to look for a key in each one of them. In the example below, `foxy-i18n` will load files for namespaces `one`, `two`, `three` and `shared`. Then, it will look for the value of `two.three.demo` in namespace called `one`, value of `three.demo` in namespace `two` and value of `demo` in namespaces `three` and `shared`. The first value found will be displayed.

```html
<foxy-i18n key="demo" ns="one two three"></foxy-i18n>
```

This is useful when building customizable forms with nested controls because developers can override the translations at any level. For situations when you're overriding the translations only at the top level, you can skip loading extra namespaces by setting the `simplify-ns-loading` attribute:

```html
<foxy-i18n key="demo" ns="one two three" simplify-ns-loading></foxy-i18n>
```

The example above will load only `one` and `shared` namespaces. Then it will look for the value of `two.three.demo` in namespace called `one` and the value of `demo` in `shared`.

To make things even easier when building complex forms, use the `infer` attribute on nested `foxy-i18n` elements and elements with `TranslatableMixin`. This will automatically infer the language, namespace and loading settings from the closest parent element:

```html
<my-translatable-form lang="en" ns="foo bar" simplify-ns-loading>
<foxy-i18n infer="" key="demo"></foxy-i18n>
</my-translatable-form>
```

In the example above `foxy-i18n` will load namespaces `foo` and `shared`. Then it will look for the value of `bar.demo` in `foo` and the value of `demo` in `shared`, displaying the one it encounters first. You can add namespaces to the `infer` attribute for more nesting:

```html
<my-translatable-form lang="en" ns="foo bar" simplify-ns-loading>
<foxy-i18n infer="baz" key="demo"></foxy-i18n>
</my-translatable-form>
```

In this example `foxy-i18n` will look for the value of `baz.bar.demo` in `foo`. If we omit `simplify-ns-loading` attribute on the form, `foxy-i18n` will download the translation file for `baz` as well.

Finally, let's pass some [options](https://www.i18next.com/translation-function/essentials#overview-options) to our I18n element. For example, to display a date:

<Canvas
mdxSource={`<foxy-i18n key="date" options='{"value":"2021-03-14"}'></foxy-i18n>`}
mdxSource={`<foxy-i18n key="demo_date" options='{"value":"2021-03-14"}'></foxy-i18n>`}
isExpanded
>
<Story name="With options">
{() => html`<foxy-i18n key="date" options='{"value":"2021-03-14"}'></foxy-i18n>`}
{() => html`<foxy-i18n key="demo_date" options='{"value":"2021-03-14"}'></foxy-i18n>`}
</Story>
</Canvas>

Expand Down
43 changes: 36 additions & 7 deletions src/mixins/translatable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,13 @@ type Base = Constructor<InferrableMixinHost> &
type Translator = (key: string, options?: StringMap) => string;

export declare class TranslatableMixinHost {
/**
* If true, this element won't attempt to load separate files for nested namespaces.
* For example, if `ns` is set to `foo bar`, this element will only load `foo` and
* expect that file to contain all translations for the `bar` namespace.
*/
simplifyNsLoading: boolean;

/** Optional ISO 639-1 code describing the language element content is written in. */
lang: string;

Expand All @@ -198,12 +205,13 @@ export const TranslatableMixin = <T extends Base>(
): T & Constructor<TranslatableMixinHost> & { defaultNS: string } => {
return class TranslatableElement extends BaseElement {
static get inferredProperties(): string[] {
return [...super.inferredProperties, 'lang', 'ns'];
return [...super.inferredProperties, 'simplifyNsLoading', 'lang', 'ns'];
}

static get properties(): PropertyDeclarations {
return {
...super.properties,
simplifyNsLoading: { type: Boolean, attribute: 'simplify-ns-loading' },
lang: { type: String },
ns: { type: String },
};
Expand All @@ -213,6 +221,8 @@ export const TranslatableMixin = <T extends Base>(
return defaultNS;
}

simplifyNsLoading = false;

lang = '';

ns = defaultNS;
Expand All @@ -222,16 +232,23 @@ export const TranslatableMixin = <T extends Base>(

if (!I18nElement) return key;

const keys = [
...this.ns
let keys: string[];

if (this.simplifyNsLoading) {
const namespaces = this.ns.split(' ').filter(v => v.length > 0);
const path = [...namespaces.slice(1), key].join('.');
keys = namespaces[0] ? [`${namespaces[0]}:${path}`] : [path];
} else {
keys = this.ns
.split(' ')
.reverse()
.map(v => v.trim())
.filter(v => v.length > 0)
.reverse()
.map((v, i, a) => `${v}:${[...a.slice(i + 1), key].join('.')}`),
`shared:${key}`,
];
.map((v, i, a) => `${v}:${[...a.slice(i + 1), key].join('.')}`);
}

keys.push(key);

return I18nElement.i18next.t(keys, { lng: this.lang, ...options }).toString();
};
Expand All @@ -249,10 +266,20 @@ export const TranslatableMixin = <T extends Base>(
super.updated(changedProperties);

const I18nElement = customElements.get('foxy-i18n') as typeof I18n | undefined;

if (!I18nElement) return;

if (changedProperties.has('lang')) I18nElement.i18next.loadLanguages(this.lang);
if (changedProperties.has('ns')) I18nElement.i18next.loadNamespaces(this.ns.split(' '));

if (changedProperties.has('ns')) {
const namespaces = this.ns.split(' ').filter(v => v.length > 0);

if (this.simplifyNsLoading) {
if (namespaces[0]) I18nElement.i18next.loadNamespaces(namespaces[0]);
} else {
I18nElement.i18next.loadNamespaces(namespaces);
}
}
}

disconnectedCallback(): void {
Expand All @@ -264,9 +291,11 @@ export const TranslatableMixin = <T extends Base>(
super.applyInferredProperties(context);
if (this.infer === null) return;

const simplifyNsLoading = context.get('simplifyNsLoading') as boolean | undefined;
const lang = context.get('lang') as string | undefined;
const ns = context.get('ns') as string | undefined;

this.simplifyNsLoading = simplifyNsLoading ?? false;
this.lang = lang ?? '';
this.ns = ns ? `${ns} ${this.infer}`.trim() : defaultNS;
}
Expand Down
4 changes: 4 additions & 0 deletions src/static/translations/shared/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"demo_key": "Hello",
"demo_date": "This is a date: {{ value, date }}"
}
3 changes: 3 additions & 0 deletions src/static/translations/shared/es.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"demo_key": "Hola"
}
1 change: 1 addition & 0 deletions src/storygen/getStory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export function getStory(params: Params): Story {
?readonly=\${args.readonly}
?hidden=\${args.hidden}
class="foxy-story__preview"
simplify-ns-loading
>
${allControls
.reduce<string[]>((slots, name) => {
Expand Down
1 change: 1 addition & 0 deletions src/storygen/getStoryCode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export function getStoryCode(summary: Summary, args: Args): TemplateResult {
${args.hidden ? 'hidden' : ''}
${args.readonly ? 'readonly' : ''}
${args.disabled ? 'disabled' : ''}
simplify-ns-loading
>
${[...sections, ...buttons, ...inputs]
.reduce<string[]>((slots, name) => {
Expand Down

0 comments on commit 751df4f

Please sign in to comment.