mirror of
https://github.com/cupcakearmy/svelte-i18n.git
synced 2024-11-16 09:59:58 +01:00
feat: 🎸 Support getting deep localized objects/arrays
✅ Closes: Closes #83
This commit is contained in:
parent
5c3191d424
commit
ff541367f8
@ -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"
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
};
|
@ -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;
|
||||
};
|
@ -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;
|
||||
});
|
||||
|
@ -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;
|
||||
|
@ -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)
|
||||
})
|
||||
})
|
@ -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' },
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user