Skip to content

Commit

Permalink
Add customizable count key
Browse files Browse the repository at this point in the history
Breaking Changes:

- Renames `count: ` to `n:` but allows customization.
  • Loading branch information
drewjbartlett committed Jan 26, 2024
1 parent 4238563 commit dfffd87
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 37 deletions.
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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"
```


Expand Down
93 changes: 64 additions & 29 deletions src/core/create-i17n.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand All @@ -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');

Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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'];
Expand Down Expand Up @@ -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');
});
Expand Down
16 changes: 11 additions & 5 deletions src/core/create-i17n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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;
Expand All @@ -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<string, string | InterpolatedFn>();
let translations: Translations = config.translations;
const loggingEnabled = Boolean(config.loggingEnabled);
Expand Down

0 comments on commit dfffd87

Please sign in to comment.