Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add mixin for handling partial i18n objects #8558

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions packages/component-base/src/i18n-mixin.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* @license
* Copyright (c) 2025 - 2025 Vaadin Ltd.
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
*/
import type { Constructor } from '@open-wc/dedupe-mixin';

/**
* A mixin that allows to set partial I18N properties.
*/
export declare function I18nMixin<T extends Constructor<HTMLElement>, I>(
superclass: T,
defaultI18n: I,
): Constructor<I18nMixinClass<I>> & T;

export declare class I18nMixinClass<I> {
/**
* The object used to localize this component. To change the default
* localization, replace this with an object that provides all properties, or
* just the individual properties you want to change.
*/
i18n: Partial<I>;
}
83 changes: 83 additions & 0 deletions packages/component-base/src/i18n-mixin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/**
* @license
* Copyright (c) 2025 - 2025 Vaadin Ltd.
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
*/

function deepMerge(target, ...sources) {
const isObject = (item) => item && typeof item === 'object' && !Array.isArray(item);
const merge = (target, source) => {
if (isObject(source) && isObject(target)) {
Object.keys(source).forEach((key) => {
if (isObject(source[key])) {
if (!target[key]) {
target[key] = {};
}

merge(target[key], source[key]);
} else if (source[key] !== undefined && source[key] !== null) {
target[key] = source[key];
}
});
}
};

sources.forEach((source) => {
merge(target, source);
});

return target;
}

/**
* A mixin that allows to set partial I18N properties.
*
* @polymerMixin
*/
export const I18nMixin = (superClass, defaultI18n) =>
class I18nMixinClass extends superClass {
static get properties() {
return {
/** @private */
__effectiveI18n: {
type: Object,
sync: true,
},
};
}

constructor() {
super();

this.i18n = deepMerge({}, defaultI18n);
}

/**
* The object used to localize this component. To change the default
* localization, replace this with an object that provides all properties, or
* just the individual properties you want to change.
*
* Should be overridden by subclasses to provide a custom JSDoc with the
* default I18N properties.
*
* @returns {Object}
*/
get i18n() {
return this.__customI18n;
}

/**
* The object used to localize this component. To change the default
* localization, replace this with an object that provides all properties, or
* just the individual properties you want to change.
*
* Should be overridden by subclasses to provide a custom JSDoc with the
* default I18N properties.
*
* @param {Object} value
*/
set i18n(value) {
this.__customI18n = value;
this.__effectiveI18n = deepMerge({}, defaultI18n, this.__customI18n);
}
};
83 changes: 83 additions & 0 deletions packages/component-base/test/i18n-mixin.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { expect } from '@vaadin/chai-plugins';
import { nextRender } from '@vaadin/testing-helpers';
import { PolymerElement } from '@polymer/polymer/polymer-element.js';
import { LitElement } from 'lit';
import { I18nMixin } from '../src/i18n-mixin.js';
import { PolylitMixin } from '../src/polylit-mixin.js';

const DEFAULT_I18N = {
foo: 'Foo',
bar: {
baz: 'Baz',
},
};

class I18nMixinPolymerElement extends I18nMixin(PolymerElement, DEFAULT_I18N) {
static get is() {
return 'i18n-mixin-polymer-element';
}
}

customElements.define(I18nMixinPolymerElement.is, I18nMixinPolymerElement);

class I18nMixinLitElement extends I18nMixin(PolylitMixin(LitElement), DEFAULT_I18N) {
static get is() {
return 'i18n-mixin-lit-element';
}
}

customElements.define(I18nMixinLitElement.is, I18nMixinLitElement);

const runTests = (baseClass) => {
let element;

beforeEach(async () => {
element = document.createElement(baseClass.is);
document.body.appendChild(element);
await nextRender();
});

it('should initialize with copy of defaults', () => {
expect(element.i18n).to.deep.equal(DEFAULT_I18N);
expect(element.i18n).to.not.equal(DEFAULT_I18N);

expect(element.__effectiveI18n).to.deep.equal(DEFAULT_I18N);
expect(element.__effectiveI18n).to.not.equal(DEFAULT_I18N);
});

it('should return same reference that was set previously', () => {
const customI18n = { foo: 'Custom Foo' };
element.i18n = customI18n;
expect(element.i18n).to.equal(customI18n);
});

it('should deep merge custom i18n with default i18n', () => {
element.i18n = {};
expect(element.__effectiveI18n).to.deep.equal(DEFAULT_I18N);

element.i18n = { foo: 'Custom Foo' };
expect(element.__effectiveI18n).to.deep.equal({ foo: 'Custom Foo', bar: { baz: 'Baz' } });

element.i18n = { bar: { baz: 'Custom Baz' } };
expect(element.__effectiveI18n).to.deep.equal({ foo: 'Foo', bar: { baz: 'Custom Baz' } });
});

it('should ignore null and undefined values in custom i18n', () => {
element.i18n = { foo: null };
expect(element.__effectiveI18n).to.deep.equal(DEFAULT_I18N);

element.i18n = { bar: undefined };
expect(element.__effectiveI18n).to.deep.equal(DEFAULT_I18N);

element.i18n = { bar: { baz: null } };
expect(element.__effectiveI18n).to.deep.equal(DEFAULT_I18N);
});
};

describe('I18nMixin + Polymer', () => {
runTests(I18nMixinPolymerElement);
});

describe('I18nMixin + Lit', () => {
runTests(I18nMixinLitElement);
});
Loading