Add pluralization support to Translation component (#4441)

* Initial plan

* Add pluralization support to Translation component with comprehensive tests

Co-authored-by: gabek <414923+gabek@users.noreply.github.com>

* Javascript formatting autofixes

* Update web/tests/translation.test.tsx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update web/components/ui/Translation/Translation.tsx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix: linter warning

* Simplify pluralization logic: use original key for plural, only _one for singular

Co-authored-by: gabek <414923+gabek@users.noreply.github.com>

* feat(i18n): fix support for nested namespace string and key extraction

* chore: update extracted translations

* fix(i18n): fix linter warnings in extraction script

* Update web/scripts/i18n-extract.js

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* feat(i18n): sort translation keys

* fix(i18n): fix linter warnings

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: gabek <414923+gabek@users.noreply.github.com>
Co-authored-by: Owncast <owncast@owncast.online>
Co-authored-by: Gabe Kangas <gabek@real-ity.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Copilot
2025-07-13 17:34:06 -07:00
committed by GitHub
co-authored by gabek Copilot copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Owncast Gabe Kangas
parent 1fd86fa190
commit 59b905649e
9 changed files with 392 additions and 71 deletions
+116
View File
@@ -14,6 +14,15 @@ jest.mock('next-export-i18n', () => ({
notification_message:
'You can <a href="#">click here</a> to receive notifications when {{streamer}} goes live.',
simple_key: 'Simple translation text',
// Pluralization test keys
item_count_one: 'You have {{count}} item',
item_count: 'You have {{count}} items', // Original key serves as plural form
message_count_one: 'You have {{count}} message from {{sender}}',
message_count: 'You have {{count}} messages from {{sender}}', // Original key serves as plural form
// Keys without pluralization variants
no_plural_key: 'This key has no plural variants - {{count}} things',
};
let result = translations[key] || key;
@@ -177,4 +186,111 @@ describe('Translation Component', () => {
// Should render the key itself as fallback
expect(screen.getByText('missing_key')).toBeInTheDocument();
});
// Pluralization tests
describe('Pluralization Support', () => {
test('should use singular form when count is 1', () => {
render(<Translation translationKey={Localization.Testing.itemCount} count={1} />);
expect(screen.getByText('You have 1 item')).toBeInTheDocument();
});
test('should use plural form when count is greater than 1', () => {
render(<Translation translationKey={Localization.Testing.itemCount} count={5} />);
expect(screen.getByText('You have 5 items')).toBeInTheDocument();
});
test('should use plural form when count is negative', () => {
render(<Translation translationKey={Localization.Testing.itemCount} count={-3} />);
expect(screen.getByText('You have -3 items')).toBeInTheDocument();
});
test('should interpolate count and other variables in pluralized text', () => {
render(
<Translation
translationKey={Localization.Testing.messageCount}
count={3}
vars={{ sender: 'Alice' }}
/>,
);
expect(screen.getByText('You have 3 messages from Alice')).toBeInTheDocument();
});
test('should interpolate count and other variables in singular text', () => {
render(
<Translation
translationKey={Localization.Testing.messageCount}
count={1}
vars={{ sender: 'Bob' }}
/>,
);
expect(screen.getByText('You have 1 message from Bob')).toBeInTheDocument();
});
test('should fallback to original key when pluralization keys do not exist', () => {
render(<Translation translationKey={Localization.Testing.noPluralKey} count={2} />);
expect(screen.getByText('This key has no plural variants - 2 things')).toBeInTheDocument();
});
test('should work without count prop (backward compatibility)', () => {
render(<Translation translationKey={Localization.Testing.simpleKey} />);
expect(screen.getByText('Simple translation text')).toBeInTheDocument();
});
test('should use defaultText with count interpolation when translation is missing', () => {
render(
<Translation
translationKey={'missing_plural_key' as any}
count={3}
defaultText="Default text with {{count}} items"
/>,
);
expect(screen.getByText('Default text with 3 items')).toBeInTheDocument();
});
test('should use defaultText with count and other vars when translation is missing', () => {
render(
<Translation
translationKey={'missing_plural_key' as any}
count={1}
vars={{ name: 'John' }}
defaultText="{{name}} has {{count}} item"
/>,
);
expect(screen.getByText('John has 1 item')).toBeInTheDocument();
});
test('should handle count of 0 as plural form', () => {
render(<Translation translationKey={Localization.Testing.itemCount} count={0} />);
expect(screen.getByText('You have 0 items')).toBeInTheDocument();
});
test('should handle fractional count as plural form', () => {
render(<Translation translationKey={Localization.Testing.itemCount} count={1.5} />);
expect(screen.getByText('You have 1.5 items')).toBeInTheDocument();
});
test('should work with className and pluralization', () => {
render(
<Translation
translationKey={Localization.Testing.itemCount}
count={2}
className="count-display"
/>,
);
const element = screen.getByText('You have 2 items');
expect(element).toHaveClass('count-display');
});
});
});