Skip to content

Commit

Permalink
feat(atomic): add localizedString directive (#4852)
Browse files Browse the repository at this point in the history
Replace the functional component with a custom lit directive

## In Stencil, we do
```ts
render () {
  return (
    <LocalizedString
      key={i18nKey}
      i18n={this.bindings.i18n}
      params={...}
    />
  ):
}
```

## In Lit, we do
```ts
render () {
  return html`
    ${localizedString({
      key: i18nKey,
      i18n: this.bindings.i18n,
      params: {...},
    })}
  `;
}
```

### Notes
It's hard to make perf adjustments to this code (e.g. using a regex
instead of splitting the string) since the directive can also accept DOM
values. For example
```ts
html`${localizedString({
  i18n: i18n,
  key: "search-instead-for",
  params: {
    query: html`
      <button
        class="link py-1"
        part="undo-btn"
        onClick={() => onClick()}
      >
        ${originalQuery}
      </button>`
  }
`
```

Which returns something like that

![image](https://github.com/user-attachments/assets/c56ea31a-b563-4832-ac8a-ecd365dad80e)

In the code example above, moving out the `<button>` Element from the
directive would imply some big refactor on the locales
https://coveord.atlassian.net/browse/KIT-3831
  • Loading branch information
y-lakhdar authored Jan 20, 2025
1 parent 5030ca2 commit b7be913
Show file tree
Hide file tree
Showing 2 changed files with 59 additions and 9 deletions.
54 changes: 54 additions & 0 deletions packages/atomic/src/directives/localized-string.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import {i18n} from 'i18next';
import {html, TemplateResult} from 'lit';
import {Directive, directive, PartInfo, PartType} from 'lit/directive.js';

export interface LocalizedStringProps {
i18n: i18n;
key: string;
params: Record<string, TemplateResult | string>;
count?: number;
}

// TODO: KIT-3822: add unit tests to this directive
class LocalizedStringDirective extends Directive {
private readonly delimitingCharacter = '\u001d'; // Unicode group separator
private readonly placeholderPrefixCharacter = '\u001a'; // Unicode substitute character

constructor(partInfo: PartInfo) {
super(partInfo);
if (partInfo.type !== PartType.CHILD) {
throw new Error('localizedString can only be used in child bindings');
}
}

render(props: LocalizedStringProps) {
const {i18n, key, params, count} = props;

const getPlaceholderForParamKey = (paramKey: string) =>
`${this.delimitingCharacter}${this.placeholderPrefixCharacter}${paramKey}${this.delimitingCharacter}`;
const getParamFromPlaceholder = (placeholder: string) =>
params[placeholder.slice(1)];

const placeholdersMap = Object.fromEntries(
Object.keys(params).map((paramKey) => [
paramKey,
getPlaceholderForParamKey(paramKey),
])
);
const localizedStringWithPlaceholders = i18n.t(key, {
interpolation: {escapeValue: false},
count: count,
...placeholdersMap,
});

return html`${localizedStringWithPlaceholders
.split(this.delimitingCharacter)
.map((text) =>
text.startsWith(this.placeholderPrefixCharacter)
? getParamFromPlaceholder(text)
: text
)}`;
}
}

export const localizedString = directive(LocalizedStringDirective);
14 changes: 5 additions & 9 deletions packages/atomic/src/utils/jsx-utils.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
import {Fragment, FunctionalComponent, h, VNode} from '@stencil/core';
import {i18n} from 'i18next';

export interface LocalizedStringProps {
i18n: i18n;
key: string;
params: Record<string, VNode | string>;
count?: number;
}
import {Fragment, FunctionalComponent, h} from '@stencil/core';
import {LocalizedStringProps} from '../directives/localized-string';

/**
* @deprecated Should only be used for Stencil components; for Lit components, use the localizedString directive instead
*/
export const LocalizedString: FunctionalComponent<LocalizedStringProps> = ({
i18n,
key,
Expand Down

0 comments on commit b7be913

Please sign in to comment.