feat: 🎸 Support getting deep localized objects/arrays

 Closes: Closes #83
This commit is contained in:
Christian Kaisermann 2020-11-05 09:44:40 -03:00
parent 5c3191d424
commit ff541367f8
12 changed files with 6919 additions and 210 deletions

1
.nvmrc
View File

@ -1 +0,0 @@
13.2.0

View File

@ -77,6 +77,7 @@
"@babel/preset-env": "^7.11.5",
"@kiwi/eslint-config": "^1.2.0",
"@kiwi/prettier-config": "^1.1.0",
"@types/dlv": "^1.1.2",
"@types/estree": "0.0.45",
"@types/intl": "^1.2.0",
"@types/jest": "^26.0.14",
@ -102,6 +103,8 @@
},
"dependencies": {
"commander": "^4.0.1",
"deepmerge": "^4.2.2",
"dlv": "^1.1.3",
"estree-walker": "^0.9.0",
"intl-messageformat": "^7.5.2",
"tiny-glob": "^0.2.6"

View File

@ -10,8 +10,8 @@ import {
import { walk } from 'estree-walker';
import { Ast } from 'svelte/types/compiler/interfaces';
import { parse } from 'svelte/compiler';
import dlv from 'dlv';
import { deepGet } from './includes/deepGet';
import { deepSet } from './includes/deepSet';
import { getObjFromExpression } from './includes/getObjFromExpression';
import { Message } from './types';
@ -191,7 +191,7 @@ export function extractMessages(
} else {
if (
overwrite === false &&
typeof deepGet(accumulator, message.meta.id) !== 'undefined'
typeof dlv(accumulator, message.meta.id) !== 'undefined'
) {
return;
}

View File

@ -1,9 +0,0 @@
export const deepGet = (o: Record<string, any>, id: string) => {
return id.split('.').reduce((acc, path) => {
if (typeof acc !== 'object') {
return acc;
}
return acc[path];
}, o);
};

View File

@ -1,17 +0,0 @@
// could use a reduce, but a simple for-in has less footprint
export const flatObj = (obj: Record<string, any>, prefix = '') => {
const flatted: Record<string, string> = {};
for (const key in obj) {
const flatKey = prefix + key;
// we want plain objects and arrays
if (typeof obj[key] === 'object') {
Object.assign(flatted, flatObj(obj[key], `${flatKey}.`));
} else {
flatted[flatKey] = obj[key];
}
}
return flatted;
};

View File

@ -1,11 +1,12 @@
import { writable, derived } from 'svelte/store';
import deepmerge from 'deepmerge';
import dlv from 'dlv';
import { LocaleDictionary, DeepDictionary, Dictionary } from '../types/index';
import { flatObj } from '../includes/flatObj';
import { LocaleDictionary, LocalesDictionary } from '../types/index';
import { getFallbackOf } from './locale';
let dictionary: Dictionary;
const $dictionary = writable<Dictionary>({});
let dictionary: LocalesDictionary;
const $dictionary = writable<LocalesDictionary>({});
export function getLocaleDictionary(locale: string) {
return (dictionary[locale] as LocaleDictionary) || null;
@ -20,15 +21,15 @@ export function hasLocaleDictionary(locale: string) {
}
export function getMessageFromDictionary(locale: string, id: string) {
if (hasLocaleDictionary(locale)) {
const localeDictionary = getLocaleDictionary(locale);
if (id in localeDictionary) {
return localeDictionary[id];
}
if (!hasLocaleDictionary(locale)) {
return null;
}
return null;
const localeDictionary = getLocaleDictionary(locale);
const match = dlv(localeDictionary, id);
return match;
}
export function getClosestAvailableLocale(locale: string): string | null {
@ -37,11 +38,9 @@ export function getClosestAvailableLocale(locale: string): string | null {
return getClosestAvailableLocale(getFallbackOf(locale));
}
export function addMessages(locale: string, ...partials: DeepDictionary[]) {
const flattedPartials = partials.map((partial) => flatObj(partial));
export function addMessages(locale: string, ...partials: LocaleDictionary[]) {
$dictionary.update((d) => {
d[locale] = Object.assign(d[locale] || {}, ...flattedPartials);
d[locale] = deepmerge.all<LocaleDictionary>([d[locale] || {}, ...partials]);
return d;
});

View File

@ -1,10 +1,11 @@
import { Formats } from 'intl-messageformat';
export interface DeepDictionary {
[key: string]: DeepDictionary | string | string[];
export interface LocaleDictionary {
[key: string]: LocaleDictionary | LocaleDictionary[] | string | string[];
}
export type LocaleDictionary = Record<string, string>;
export type Dictionary = Record<string, LocaleDictionary>;
export type LocalesDictionary = {
[key: string]: LocaleDictionary;
};
export interface MessageObject {
id?: string;

View File

@ -1,17 +0,0 @@
import { deepGet } from '../../src/cli/includes/deepGet'
describe('deep object handling', () => {
test('gets a deep property', () => {
const obj = {
a: { b: { c: { d: ['foo', 'bar'] } } },
}
expect(deepGet(obj, 'a.b.c.d.1')).toBe('bar')
})
test('returns undefined for if some property not found', () => {
const obj = {
a: { b: 1 },
}
expect(deepGet(obj, 'c.b')).toBe(undefined)
})
})

View File

@ -1,51 +1,82 @@
import { lookup, lookupCache } from '../../../src/runtime/includes/lookup'
import { $dictionary, addMessages } from '../../../src/runtime/stores/dictionary'
import { lookup, lookupCache } from '../../../src/runtime/includes/lookup';
import {
$dictionary,
addMessages,
} from '../../../src/runtime/stores/dictionary';
beforeEach(() => {
$dictionary.set({})
})
$dictionary.set({});
});
test('returns null if no locale was passed', () => {
expect(lookup('message.id', undefined)).toBe(null)
expect(lookup('message.id', null)).toBe(null)
})
expect(lookup('message.id', undefined)).toBeNull();
expect(lookup('message.id', null)).toBeNull();
});
test('gets a shallow message of a locale dictionary', () => {
addMessages('en', { field: 'name' })
addMessages('en', { field: 'name' });
expect(lookup('field', 'en')).toBe('name')
})
expect(lookup('field', 'en')).toBe('name');
});
test('gets a deep message of a locale dictionary', () => {
addMessages('en', { deep: { field: 'lastname' } })
expect(lookup('deep.field', 'en')).toBe('lastname')
})
addMessages('en', { deep: { field: 'lastname' } });
expect(lookup('deep.field', 'en')).toBe('lastname');
});
test('gets a message from the fallback dictionary', () => {
addMessages('en', { field: 'name' })
addMessages('en', { field: 'name' });
expect(lookup('field', 'en-US')).toBe('name')
})
expect(lookup('field', 'en-US')).toBe('name');
});
test('gets an array ', () => {
addMessages('en', {
careers: [
{
role: 'Role 1',
description: 'Description 1',
},
{
role: 'Role 2',
description: 'Description 2',
},
],
});
expect(lookup('careers', 'en-US')).toMatchInlineSnapshot(`
Array [
Object {
"description": "Description 1",
"role": "Role 1",
},
Object {
"description": "Description 2",
"role": "Role 2",
},
]
`);
});
test('caches found messages by locale', () => {
addMessages('en', { field: 'name' })
addMessages('pt', { field: 'nome' })
lookup('field', 'en-US')
lookup('field', 'pt')
addMessages('en', { field: 'name' });
addMessages('pt', { field: 'nome' });
lookup('field', 'en-US');
lookup('field', 'pt');
expect(lookupCache).toMatchObject({
'en-US': { field: 'name' },
pt: { field: 'nome' },
})
})
});
});
test("doesn't cache falsy messages", () => {
addMessages('en', { field: 'name' })
addMessages('pt', { field: 'nome' })
lookup('field_2', 'en-US')
lookup('field_2', 'pt')
addMessages('en', { field: 'name' });
addMessages('pt', { field: 'nome' });
lookup('field_2', 'en-US');
lookup('field_2', 'pt');
expect(lookupCache).not.toMatchObject({
'en-US': { field_2: 'name' },
pt: { field_2: 'nome' },
})
})
});
});

View File

@ -4,71 +4,44 @@ import {
getLocaleFromNavigator,
getLocaleFromPathname,
getLocaleFromHostname,
} from '../../../src/runtime/includes/localeGetters'
import { flatObj } from '../../../src/runtime/includes/flatObj'
} from '../../../src/runtime/includes/localeGetters';
describe('getting client locale', () => {
beforeEach(() => {
delete window.location
delete window.location;
window.location = {
pathname: '/',
hostname: 'example.com',
hash: '',
search: '',
} as any
})
} as any;
});
test('gets the locale based on the passed hash parameter', () => {
window.location.hash = '#locale=en-US&lang=pt-BR'
expect(getLocaleFromHash('lang')).toBe('pt-BR')
})
it('gets the locale based on the passed hash parameter', () => {
window.location.hash = '#locale=en-US&lang=pt-BR';
expect(getLocaleFromHash('lang')).toBe('pt-BR');
});
test('gets the locale based on the passed search parameter', () => {
window.location.search = '?locale=en-US&lang=pt-BR'
expect(getLocaleFromQueryString('lang')).toBe('pt-BR')
})
it('gets the locale based on the passed search parameter', () => {
window.location.search = '?locale=en-US&lang=pt-BR';
expect(getLocaleFromQueryString('lang')).toBe('pt-BR');
});
test('gets the locale based on the navigator language', () => {
expect(getLocaleFromNavigator()).toBe(window.navigator.language)
})
it('gets the locale based on the navigator language', () => {
expect(getLocaleFromNavigator()).toBe(window.navigator.language);
});
test('gets the locale based on the pathname', () => {
window.location.pathname = '/en-US/foo/'
expect(getLocaleFromPathname(/^\/(.*?)\//)).toBe('en-US')
})
it('gets the locale based on the pathname', () => {
window.location.pathname = '/en-US/foo/';
expect(getLocaleFromPathname(/^\/(.*?)\//)).toBe('en-US');
});
test('gets the locale base on the hostname', () => {
window.location.hostname = 'pt.example.com'
expect(getLocaleFromHostname(/^(.*?)\./)).toBe('pt')
})
it('gets the locale base on the hostname', () => {
window.location.hostname = 'pt.example.com';
expect(getLocaleFromHostname(/^(.*?)\./)).toBe('pt');
});
test('returns null if no locale was found', () => {
expect(getLocaleFromQueryString('lang')).toBe(null)
})
})
describe('deep object handling', () => {
test('flattens a deep object', () => {
const obj = {
a: { b: { c: { d: 'foo' } } },
e: { f: 'bar' },
}
expect(flatObj(obj)).toMatchObject({
'a.b.c.d': 'foo',
'e.f': 'bar',
})
})
test('flattens a deep object with array values', () => {
const obj = {
a: { b: { c: { d: ['foo', 'bar'] } } },
e: { f: ['foo', 'bar'] },
}
expect(flatObj(obj)).toMatchObject({
'a.b.c.d.0': 'foo',
'a.b.c.d.1': 'bar',
'e.f.0': 'foo',
'e.f.1': 'bar',
})
})
})
it('returns null if no locale was found', () => {
expect(getLocaleFromQueryString('lang')).toBeNull();
});
});

View File

@ -1,4 +1,4 @@
import { get } from 'svelte/store'
import { get } from 'svelte/store';
import {
getDictionary,
@ -9,107 +9,105 @@ import {
$dictionary,
$locales,
getLocaleDictionary,
} from '../../../src/runtime/stores/dictionary'
} from '../../../src/runtime/stores/dictionary';
beforeEach(() => {
$dictionary.set({})
})
$dictionary.set({});
});
test('adds a new dictionary to a locale', () => {
addMessages('en', { field_1: 'name' })
addMessages('pt', { field_1: 'nome' })
addMessages('en', { field_1: 'name' });
addMessages('pt', { field_1: 'nome' });
expect(get($dictionary)).toMatchObject({
en: { field_1: 'name' },
pt: { field_1: 'nome' },
})
})
});
});
test('gets the whole current dictionary', () => {
addMessages('en', { field_1: 'name' })
expect(getDictionary()).toMatchObject(get($dictionary))
})
addMessages('en', { field_1: 'name' });
expect(getDictionary()).toMatchObject(get($dictionary));
});
test('merges the existing dictionaries with new ones', () => {
addMessages('en', { field_1: 'name', deep: { prop1: 'foo' } })
addMessages('en', { field_2: 'lastname', deep: { prop2: 'foo' } })
addMessages('pt', { field_1: 'nome', deep: { prop1: 'foo' } })
addMessages('pt', { field_2: 'sobrenome', deep: { prop2: 'foo' } })
addMessages('en', { field_1: 'name', deep: { prop1: 'foo' } });
addMessages('en', { field_2: 'lastname', deep: { prop2: 'foo' } });
addMessages('pt', { field_1: 'nome', deep: { prop1: 'foo' } });
addMessages('pt', { field_2: 'sobrenome', deep: { prop2: 'foo' } });
expect(get($dictionary)).toMatchObject({
en: {
field_1: 'name',
field_2: 'lastname',
'deep.prop1': 'foo',
'deep.prop2': 'foo',
deep: { prop1: 'foo', prop2: 'foo' },
},
pt: {
field_1: 'nome',
field_2: 'sobrenome',
'deep.prop1': 'foo',
'deep.prop2': 'foo',
deep: { prop1: 'foo', prop2: 'foo' },
},
})
})
});
});
test('gets the dictionary of a locale', () => {
addMessages('en', { field_1: 'name' })
expect(getLocaleDictionary('en')).toMatchObject({ field_1: 'name' })
})
addMessages('en', { field_1: 'name' });
expect(getLocaleDictionary('en')).toMatchObject({ field_1: 'name' });
});
test('checks if a locale dictionary exists', () => {
addMessages('pt', { field_1: 'name' })
expect(hasLocaleDictionary('en')).toBe(false)
expect(hasLocaleDictionary('pt')).toBe(true)
})
addMessages('pt', { field_1: 'name' });
expect(hasLocaleDictionary('en')).toBe(false);
expect(hasLocaleDictionary('pt')).toBe(true);
});
test('gets the closest available locale', () => {
addMessages('pt', { field_1: 'name' })
expect(getClosestAvailableLocale('pt-BR')).toBe('pt')
})
addMessages('pt', { field_1: 'name' });
expect(getClosestAvailableLocale('pt-BR')).toBe('pt');
});
test("returns null if there's no closest locale available", () => {
addMessages('pt', { field_1: 'name' })
expect(getClosestAvailableLocale('it-IT')).toBe(null)
})
addMessages('pt', { field_1: 'name' });
expect(getClosestAvailableLocale('it-IT')).toBeNull();
});
test('lists all locales in the dictionary', () => {
addMessages('en', {})
addMessages('pt', {})
addMessages('pt-BR', {})
expect(get($locales)).toEqual(['en', 'pt', 'pt-BR'])
})
addMessages('en', {});
addMessages('pt', {});
addMessages('pt-BR', {});
expect(get($locales)).toEqual(['en', 'pt', 'pt-BR']);
});
describe('getting messages', () => {
test('gets a message from a shallow dictionary', () => {
addMessages('en', { message: 'Some message' })
expect(getMessageFromDictionary('en', 'message')).toBe('Some message')
})
it('gets a message from a shallow dictionary', () => {
addMessages('en', { message: 'Some message' });
expect(getMessageFromDictionary('en', 'message')).toBe('Some message');
});
test('gets a message from a deep object in the dictionary', () => {
addMessages('en', { messages: { message_1: 'Some message' } })
it('gets a message from a deep object in the dictionary', () => {
addMessages('en', { messages: { message_1: 'Some message' } });
expect(getMessageFromDictionary('en', 'messages.message_1')).toBe(
'Some message'
)
})
'Some message',
);
});
test('gets a message from an array in the dictionary', () => {
addMessages('en', { messages: ['Some message', 'Other message'] })
expect(getMessageFromDictionary('en', 'messages.0')).toBe('Some message')
expect(getMessageFromDictionary('en', 'messages.1')).toBe('Other message')
})
it('gets a message from an array in the dictionary', () => {
addMessages('en', { messages: ['Some message', 'Other message'] });
expect(getMessageFromDictionary('en', 'messages.0')).toBe('Some message');
expect(getMessageFromDictionary('en', 'messages.1')).toBe('Other message');
});
test('accepts english in dictionary keys', () => {
it('accepts english in dictionary keys', () => {
addMessages('pt', {
'Hey man, how are you today?': 'E ai cara, como você vai hoje?',
})
});
expect(getMessageFromDictionary('pt', 'Hey man, how are you today?')).toBe(
'E ai cara, como você vai hoje?'
)
})
'E ai cara, como você vai hoje?',
);
});
test('returns null for missing messages', () => {
addMessages('en', {})
expect(getMessageFromDictionary('en', 'foo')).toBe(null)
})
})
it('returns undefined for missing messages', () => {
addMessages('en', {});
expect(getMessageFromDictionary('en', 'foo')).toBeUndefined();
});
});

6748
yarn.lock Normal file

File diff suppressed because it is too large Load Diff