diff --git a/README.md b/README.md index 54dc338..8223f1b 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ A lightweight (**1.4kB**) internationalization library tailored for startups and - ✅ basic `{ "key": "value" }` translations `t('key')` - ✅ nested `{ "key": "child": "value" }` translations `t('key.child')` - ✅ interpolated values `t('Hello, {name}!', { name: 'Drew' })` -- ✅ count based translations `t('tree', { count: 1 }) // "tree"`, `t('tree', { count: 10 }) // "trees"` +- ✅ count based translations `t('tree', { n: 1 }) // "tree"`, `t('tree', { n: 10 }) // "trees"` - ✅ smart caching - after a key is resolved once it is read from a cache - ✅ extending core translations - ✅ lightweight - 1.4kB @@ -107,6 +107,7 @@ $t('helloWorld') | `translations` | The core `{ key: value }` translations. | | `loggingEnabled` | When enabled warning logs will write to the console for missing keys. | | `cache` | Optionally pass a prebuilt cache of the resolved `{ key: value }` pairs. | +| `countKey` | Optionally customize the count key. Defaults to `n` (`$t('user', { n: 1 })`) | ### API @@ -151,8 +152,8 @@ i17n.t('withAnInterpolation', { name: 'Robin' }) // "Hey, Robin!" Keeping with the example above, there are times when a translation should resolve based on a given count. To do this simply provide a key of the same name and postfix it with `__one` or `__many`. ```ts -i17n.t('withCounts.mouse', { count: 1 }) // "Mouse" -i17n.t('withCounts.mouse', { count: 10 }) // "Mice" +i17n.t('withCounts.mouse', { n: 1 }) // "Mouse" +i17n.t('withCounts.mouse', { n: 10 }) // "Mice" ``` diff --git a/src/core/create-i17n.test.ts b/src/core/create-i17n.test.ts index c0ad4bc..254bf1f 100644 --- a/src/core/create-i17n.test.ts +++ b/src/core/create-i17n.test.ts @@ -104,7 +104,7 @@ describe('createI17n', () => { expect(spy).toHaveBeenCalledWith('welcomeUser', expect.any(Function)); }); - it('should resolve a primitive value from the cache', () => { + it('resolves a primitive value from the cache', () => { const setSpy = jest.spyOn(cache, 'set'); const getSpy = jest.spyOn(cache, 'get'); @@ -122,7 +122,7 @@ describe('createI17n', () => { expect(getSpy).toHaveBeenCalledTimes(4); }); - it('should resolve a function from the cache', () => { + it('resolves a function from the cache', () => { const setSpy = jest.spyOn(cache, 'set'); const getSpy = jest.spyOn(cache, 'get'); @@ -145,13 +145,13 @@ describe('createI17n', () => { const getSpy = jest.spyOn(cache, 'get'); // no cache hit - expect(t('project.name', { count: 1 })).toBe(lang.project.name__one); + expect(t('project.name', { n: 1 })).toBe(lang.project.name__one); // cache hits (4) - expect(t('project.name', { count: 1 })).toBe(lang.project.name__one); - expect(t('project.name', { count: 1 })).toBe(lang.project.name__one); - expect(t('project.name', { count: 1 })).toBe(lang.project.name__one); - expect(t('project.name', { count: 1 })).toBe(lang.project.name__one); + expect(t('project.name', { n: 1 })).toBe(lang.project.name__one); + expect(t('project.name', { n: 1 })).toBe(lang.project.name__one); + expect(t('project.name', { n: 1 })).toBe(lang.project.name__one); + expect(t('project.name', { n: 1 })).toBe(lang.project.name__one); expect(setSpy).toHaveBeenCalledWith('project.name__one', lang.project.name__one); expect(setSpy).toHaveBeenCalledTimes(1); @@ -163,13 +163,13 @@ describe('createI17n', () => { const getSpy = jest.spyOn(cache, 'get'); // no cache hit - t('project.name', { count: 1 }); - t('project.name', { count: 0 }); + t('project.name', { n: 1 }); + t('project.name', { n: 0 }); // cache hits (3) - expect(t('project.name', { count: 1 })).toBe(lang.project.name__one); - expect(t('project.name', { count: 1 })).toBe(lang.project.name__one); - expect(t('project.name', { count: 0 })).toBe(lang.project.name__many); + expect(t('project.name', { n: 1 })).toBe(lang.project.name__one); + expect(t('project.name', { n: 1 })).toBe(lang.project.name__one); + expect(t('project.name', { n: 0 })).toBe(lang.project.name__many); expect(setSpy).toHaveBeenCalledWith('project.name__one', lang.project.name__one); expect(setSpy).toHaveBeenCalledWith('project.name__many', lang.project.name__many); @@ -196,37 +196,72 @@ describe('createI17n', () => { ({ t } = createI17n({ translations: langWithCounts })); }); - it('should find the the _one key when the count is one', () => { - expect(t('comment', { count: 1 })).toBe(langWithCounts.comment__one); + it('finds the the _one key when the count is one', () => { + expect(t('comment', { n: 1 })).toBe(langWithCounts.comment__one); }); - it('should find the _many key when the number is 0', () => { - expect(t('comment', { count: 0 })).toBe(langWithCounts.comment__many); + it('finds the _many key when the number is 0', () => { + expect(t('comment', { n: 0 })).toBe(langWithCounts.comment__many); }); - it('should find the _many key when the number is greater than 1', () => { - expect(t('comment', { count: 10 })).toBe(langWithCounts.comment__many); + it('finds the _many key when the number is greater than 1', () => { + expect(t('comment', { n: 10 })).toBe(langWithCounts.comment__many); }); - it('should resolve to the key passed when there is a count and no count based interpolations', () => { - expect(t('user', { count: 10 })).toBe(langWithCounts.user); + it('resolves to the key passed when there is a count and no count based interpolations', () => { + expect(t('user', { n: 10 })).toBe(langWithCounts.user); }); describe('nested', () => { - it('should find the the _one key when the count is one', () => { - expect(t('project.name', { count: 1 })).toBe(langWithCounts.project.name__one); + it('finds the the _one key when the count is one', () => { + expect(t('project.name', { n: 1 })).toBe(langWithCounts.project.name__one); }); - it('should find the _many key when the number is greater than 1', () => { - expect(t('project.name', { count: 10 })).toBe(langWithCounts.project.name__many); + it('finds the _many key when the number is greater than 1', () => { + expect(t('project.name', { n: 10 })).toBe(langWithCounts.project.name__many); }); - it('should resolve to the key passed when there is a count and no count based interpolations', () => { - expect(t('project.name', { count: 10 })).toBe(langWithCounts.project.name__many); + it('resolves to the key passed when there is a count and no count based interpolations', () => { + expect(t('project.name', { n: 10 })).toBe(langWithCounts.project.name__many); }); }); }); + describe('count based with custom key', () => { + const langWithCounts = { + comment__one: 'Comment', + comment__many: 'Comments', + + project: { + name__one: 'Project', + name__many: 'Projects', + }, + + user: 'User', + }; + let t: i17n['t']; + + beforeAll(() => { + ({ t } = createI17n({ translations: langWithCounts, countKey: 'count' })); + }); + + it('finds the the _one key when the count is one', () => { + expect(t('comment', { count: 1 })).toBe(langWithCounts.comment__one); + }); + + it('finds the _many key when the number is 0', () => { + expect(t('comment', { count: 0 })).toBe(langWithCounts.comment__many); + }); + + it('finds the _many key when the number is greater than 1', () => { + expect(t('comment', { count: 10 })).toBe(langWithCounts.comment__many); + }); + + it('resolves to the key passed when there is a count and no count based interpolations', () => { + expect(t('user', { count: 10 })).toBe(langWithCounts.user); + }); + }); + describe('extend', () => { let t: i17n['t']; let extend: i17n['extend']; @@ -256,9 +291,9 @@ describe('createI17n', () => { it('should have the keys once extended', () => { expect(t('aNewKey')).toBe('here'); expect(t('aNewInterpolated', { new: 'newer' })).toBe('a newer one'); - expect(t('aNewWithCount', { count: 1 })).toBe('singular'); - expect(t('aNewWithCount', { count: 2 })).toBe('plural'); - expect(t('a.newNested.item', { count: 2 })).toBe('a new nested item'); + expect(t('aNewWithCount', { n: 1 })).toBe('singular'); + expect(t('aNewWithCount', { n: 2 })).toBe('plural'); + expect(t('a.newNested.item', { n: 2 })).toBe('a new nested item'); expect(t('global.save', { item: 'foo' })).toBe('Save foo'); expect(t('global.buttons.edit')).toBe('Edit'); }); diff --git a/src/core/create-i17n.ts b/src/core/create-i17n.ts index 022d721..5e643fc 100644 --- a/src/core/create-i17n.ts +++ b/src/core/create-i17n.ts @@ -29,6 +29,12 @@ export interface I17nConfig { */ loggingEnabled?: boolean; + /** + * The key that will be used to determine if we should do a lookup for __one or __many. + * Default: 'n' + */ + countKey?: string; + /** * Optionally pass a prebuilt cache of the resolved { key: value } pairs. */ @@ -56,11 +62,6 @@ export enum CountValueIndicators { Many = 'many', } -/** - * The interpolations key that determines if we should do a lookup for __one or __many (CountValueIndicators). - */ -export const COUNT_KEY = 'count'; - function getCountValue(count: number): CountValueIndicators { if (count === 0) { return CountValueIndicators.Many; @@ -77,6 +78,11 @@ function buildCountValueKey(k: string, count: number): string { * Initialize the core i17n instance. */ export function createI17n(config: I17nConfig): i17n { + /** + * The interpolations key that determines if we should do a lookup for __one or __many (CountValueIndicators). + */ + const COUNT_KEY = config.countKey || 'n'; + const cache = config.cache || new Map(); let translations: Translations = config.translations; const loggingEnabled = Boolean(config.loggingEnabled);