diff --git a/lib/i18n.js b/lib/i18n.js index a3a124c..726b82e 100644 --- a/lib/i18n.js +++ b/lib/i18n.js @@ -1,4 +1,3 @@ -const lodash = require('lodash') const tools = require('extras') const DEFAULT_LOCALES = require('./locales.js') @@ -16,16 +15,38 @@ i18n.t = function (options = {}) { options.locales = DEFAULT_LOCALES } - const locales = lodash.merge({}, options.locales) + const locales = {} + for (const lang in options.locales) { + locales[lang] = flattenObjectToMap(options.locales[lang]) + } return function (path, ...args) { + if (!/^[a-z0-9_. %-]+$/.test(path)) return path + if (path.startsWith('__')) return path + try { - const value = lodash.get(locales[options.lang], path) || path + const value = locales[options.lang].get(path) || path return tools.format(value, ...args) } catch (e) { return path } } + + function flattenObjectToMap(obj, prefix = undefined) { + const map = new Map() + for (const [key, value] of Object.entries(obj)) { + const fullKey = prefix ? `${prefix}.${key}` : key + if (value && typeof value === 'object' && !Array.isArray(value)) { + const nestedMap = flattenObjectToMap(value, fullKey) + for (const [nestedKey, nestedValue] of nestedMap) { + map.set(nestedKey, nestedValue) + } + } else { + map.set(fullKey, value) + } + } + return map + } } // Link function diff --git a/test/compile.test.js b/test/compile.test.js index 6c48412..e6250bf 100644 --- a/test/compile.test.js +++ b/test/compile.test.js @@ -1,4 +1,5 @@ const compile = require('../lib/compile.js') +const i18n = require('../lib/i18n.js') describe('compile', () => { it('should not compile templates if no matches', async () => { @@ -43,4 +44,28 @@ describe('compile', () => { compile($) expect($.page.content).toBe("hello ${'wordbye'} bye ${'wordbye'}") }) + + it('should disallow most characters for t', async () => { + const $ = { + page: { content: "hello ${$.t('invalid/chars')}" }, + t: i18n.t({ + lang: 'en', + locales: { en: { 'invalid/chars': 'something' } } + }) + } + compile($) + expect($.page.content).toBe("hello ${'invalid/chars'}") + }) + + it('should disallow "__" lookups for t', async () => { + const $ = { + page: { content: "hello ${$.t('__whatever')}" }, + t: i18n.t({ + lang: 'en', + locales: { en: { __whatever: 'something' } } + }) + } + compile($) + expect($.page.content).toBe("hello ${'__whatever'}") + }) }) diff --git a/test/i18n.test.js b/test/i18n.test.js index 31a5f5e..b757f5e 100644 --- a/test/i18n.test.js +++ b/test/i18n.test.js @@ -67,6 +67,33 @@ describe('t', () => { const result = $t('eq', 'hello') expect(result).toBe('must be equal to hello') }) + + it('should allow nested locales', async () => { + const $t = i18n.t({ + lang: 'en', + locales: { + en: { + first: { + second: 'something' + }, + not_nested: 'something else' + } + } + }) + + const result1 = $t('first.second') + expect(result1).toBe('something') + + const result2 = $t('not_nested') + expect(result2).toBe('something else') + }) + + it('should not allow access to arbitrary properties', async () => { + const $t = i18n.t({ locales: LOCALES }) + // Note that __proto__ is another "magic" property, but it appears to be undefined in Node (compared to some/all browsers) + const result = $t('__defineGetter__') + expect(result).toBe('__defineGetter__') + }) }) describe('link', () => {