From d41acd2776b50d7aaf1ade279c3e4d494614e758 Mon Sep 17 00:00:00 2001 From: Mitchell Cohen Date: Wed, 6 Sep 2017 11:28:36 -0400 Subject: [PATCH] 0.2.0 --- .gitignore | 3 +- LICENSE.md | 8 + dist/es6/extract-strings/extract-strings.d.ts | 0 dist/es6/extract-strings/extract-strings.js | 96 ---- dist/es6/i18n.js | 151 ----- dist/es6/index.d.ts | 79 +-- dist/es6/index.js | 211 +------ dist/index.d.ts | 79 +-- dist/index.js | 235 +------- package-lock.json | 520 ++++++++++++++++++ package.json | 19 +- src/format.ts | 58 ++ src/helpers.ts | 26 + src/icu.ts | 35 ++ src/index.ts | 319 +---------- src/react.ts | 37 ++ src/t-i18n.ts | 131 +++++ src/tsconfig.es6.json | 2 + src/tsconfig.json | 2 + src/types.ts | 44 ++ test/format.test.ts | 45 ++ test/{i18n.test.ts => t-i18n.test.ts} | 59 +- 22 files changed, 950 insertions(+), 1209 deletions(-) create mode 100644 LICENSE.md delete mode 100644 dist/es6/extract-strings/extract-strings.d.ts delete mode 100644 dist/es6/extract-strings/extract-strings.js delete mode 100644 dist/es6/i18n.js create mode 100644 package-lock.json create mode 100644 src/format.ts create mode 100644 src/helpers.ts create mode 100644 src/icu.ts create mode 100644 src/react.ts create mode 100644 src/t-i18n.ts create mode 100644 src/types.ts create mode 100644 test/format.test.ts rename test/{i18n.test.ts => t-i18n.test.ts} (73%) diff --git a/.gitignore b/.gitignore index 40b878d..04c01ba 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -node_modules/ \ No newline at end of file +node_modules/ +dist/ \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..1d51db7 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,8 @@ +Copyright (c) 2017 AgileBits Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/dist/es6/extract-strings/extract-strings.d.ts b/dist/es6/extract-strings/extract-strings.d.ts deleted file mode 100644 index e69de29..0000000 diff --git a/dist/es6/extract-strings/extract-strings.js b/dist/es6/extract-strings/extract-strings.js deleted file mode 100644 index 037f9a4..0000000 --- a/dist/es6/extract-strings/extract-strings.js +++ /dev/null @@ -1,96 +0,0 @@ -import * as ts from "typescript"; -import * as fs from "fs"; -import * as glob from "glob"; -import T, { Plural, generator } from "../index"; -const helpers = { - "Plural": Plural, - "generator": generator -}; -let currentPrefix = ""; -function evaluate(node, src) { - if (!node) - return null; - if (node.text) - return node.text; - const expression = node.getFullText(src); - // evil eval - // this allows the use of translation helper objects/functions in messages and IDs - const evalFunc = Function(...["Plural", "generator"], "return " + expression); - const res = evalFunc(helpers.Plural, helpers.generator); - return res; -} -function findMethodCall(objectName, methodName, src) { - let res = []; - find(src); - function find(node) { - if (!node) - return; - if (node.kind === ts.SyntaxKind.CallExpression) { - const call = node; - if (call.expression.kind && call.expression.kind === ts.SyntaxKind.PropertyAccessExpression) { - const methodCall = call.expression, obj = methodCall.expression, method = methodCall.name; - if (obj.text === objectName && method.text === methodName) { - res.push(node); - } - } - } - return ts.forEachChild(node, find); - } - return res; -} -function findFunctionCall(tagName, src) { - let res = []; - find(src); - function find(node) { - if (!node) - return; - if (node.kind === ts.SyntaxKind.CallExpression) { - const call = node, expression = call.expression, name = expression; - if (expression && name.text === tagName) { - res.push(node); - } - } - return ts.forEachChild(node, find); - } - return res; -} -function extractPrefix(contents) { - const srcFile = ts.createSourceFile("file.ts", contents, ts.ScriptTarget.ES2017, false, ts.ScriptKind.TSX); - const setupCalls = findMethodCall("i18n", "withPrefix", srcFile); - setupCalls.forEach(c => { - currentPrefix = c.arguments[0].text; - }); -} -function extractMessages(contents) { - const srcFile = ts.createSourceFile("file.ts", contents, ts.ScriptTarget.ES2017, false, ts.ScriptKind.TSX); - const tCalls = findFunctionCall("T", srcFile); - let messages = {}; - tCalls.forEach(c => { - const [message, values, id] = c.arguments; - const evaluatedMessage = evaluate(message, srcFile); - let idText = id ? id.text : T._i18nInstance.generateId(evaluatedMessage); - if (currentPrefix) - idText = currentPrefix + "." + idText; - messages[idText] = evaluatedMessage; - }); - return messages; -} -function runner(err, files) { - console.log(files); - let allMessages = {}; - files.forEach(file => { - currentPrefix = ""; - const contents = fs.readFileSync(file).toString(); - extractPrefix(contents); - const messages = extractMessages(contents); - allMessages = Object.assign({}, allMessages, messages); - }); - const sortedMessages = {}; - Object.keys(allMessages).sort().forEach(function (key) { - sortedMessages[key] = allMessages[key]; - }); - fs.writeFileSync(outPath, JSON.stringify(sortedMessages, null, "\t"), { encoding: "utf8" }); -} -const input = process.argv[2] || process.cwd() + "/**/*.ts"; -const outPath = process.argv[3] || process.cwd() + "/messages.en.json"; -glob(input, runner); diff --git a/dist/es6/i18n.js b/dist/es6/i18n.js deleted file mode 100644 index 569a293..0000000 --- a/dist/es6/i18n.js +++ /dev/null @@ -1,151 +0,0 @@ -/** - * t-i18n - lightweight localization - * v0.1 - * - * i18n defers to standards to do the hard work of localization. The browser Intl API is use to format - * dates and numbers. Messages are provided as functions rather than strings, so they can be compiled at build time. - * - * - * Cobbled together by Mitch Cohen on July 31, 2017 - */ -export let dateTimeFormatOptions = { - short: { - month: 'short', - day: 'numeric', - year: "numeric" - }, - long: { - month: 'long', - day: 'numeric', - year: 'numeric' - }, - dateTime: { - month: 'short', - day: 'numeric', - year: 'numeric', - hour: 'numeric', - minute: 'numeric' - } -}; -dateTimeFormatOptions.default = dateTimeFormatOptions.long; -export let numberFormatOptions = { - currency: { - style: 'currency' - }, - decimal: { - style: 'decimal' - }, - percent: { - style: 'percent' - } -}; -numberFormatOptions.default = numberFormatOptions.decimal; -export const collationTable = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ#'.split(''); -export const defaultLanguage = "en"; -const spaceRegex = /\s/g; -const nonWordRegex = /\W/g; -export const generator = { - plain(message) { - return message; - }, - hyphens(message) { - const hyphenated = message.toLowerCase().trim().replace(spaceRegex, "-").replace(nonWordRegex, '-'); - return hyphenated; - } -}; -// helper function to produce a pluralized string -export function Plural(pluralizeFor, options) { - const { zero, one, other } = options; - return "{" + pluralizeFor + ", plural,\n" + - (zero ? "\t=0{" + zero + "}\n" : "") + - (one ? "\tone{" + one + "}\n" : "") + - ("\tother{" + other + "}}"); -} -// Create cached versions of Intl.DateTimeFormat and Intl.NumberFormat -export function createCachedFormatter(intlFormat) { - let cache = {}; - return function (locale, formatOptions) { - const args = Array.prototype.slice.call(arguments); - const id = locale + "-" + JSON.stringify(formatOptions); - if (id in cache) - return cache[id]; - const formatter = new (Function.prototype.bind.call(intlFormat, null, ...args)); - cache[id] = formatter; - return formatter; - }; -} -class I18n { - constructor(options = null) { - this.setup(Object.assign({}, I18n.defaultSetup, options)); - this.dateFormatter = createCachedFormatter(Intl.DateTimeFormat); - this.numberFormatter = createCachedFormatter(Intl.NumberFormat); - } - format(type, value, formatStyle, locale = this.locale) { - const options = (type === "date") ? dateTimeFormatOptions : numberFormatOptions; - const formatter = (type === "date") ? this.dateFormatter : this.numberFormatter; - return formatter.call(this, locale, (options[formatStyle] || options.default)).format(value); - } - mFuncForKey(key, locale = this.locale) { - const d = this.messages[locale]; - if (!d || !d[key]) - return; - if (typeof d[key] === "string") - return this.compiler(d[key]); - return d[key]; - } - generateId(message) { - return this.idGenerator(message); - } - lookup(id, replacements = null, defaultMFunc) { - if (!defaultMFunc) - defaultMFunc = () => id; - let mFunc = this.mFuncForKey(id, this.locale) || this.mFuncForKey(id, defaultLanguage); - if (!mFunc) - mFunc = defaultMFunc; - return mFunc(replacements); - } - setup(options = {}) { - const { locale, idGenerator, messages } = options; - if (idGenerator) - this.idGenerator = idGenerator; - if (locale) - this.locale = locale; - if (messages) - this.messages = messages; - return { - messages: this.messages, - locale: this.locale, - idGenerator: this.idGenerator - }; - } -} -I18n.defaultSetup = { - messages: {}, - locale: defaultLanguage, - idGenerator: generator.hyphens, - compiler: (message) => () => message -}; -function createT(context) { - let T = function translate(message, replacements, id) { - if (!id) - id = context.generateId(message); - let defaultMFunc = context.mFuncForKey(id, defaultLanguage); - if (!defaultMFunc) - defaultMFunc = () => message; - return context.lookup(id, replacements, defaultMFunc); - }; - T._i18nInstance = context; - T.setup = context.setup.bind(T._i18nInstance); - T.lookup = context.lookup.bind(T._i18nInstance); - T.date = context.format.bind(T._i18nInstance, "date"); - T.number = context.format.bind(T._i18nInstance, "number"); - return T; -} -function i18nNamespace() { - let i18nInstance = new I18n(); - return createT(i18nInstance); -} -// singleton -export default i18nNamespace(); -// or roll your own -export const makeI18n = i18nNamespace; diff --git a/dist/es6/index.d.ts b/dist/es6/index.d.ts index 003fe96..428120a 100644 --- a/dist/es6/index.d.ts +++ b/dist/es6/index.d.ts @@ -1,77 +1,2 @@ -/// -/** - * T.js - lightweight localization - * v0.2 - * - * T.js defers to standards to do the hard work of localization. The browser Intl API is use to format - * dates and numbers. Messages are provided as functions rather than strings, so they can be compiled at build time. - * - * - * Cobbled together by Mitch Cohen on July 31, 2017 - */ -export declare type MFunc = (replacements?: Replacements) => string; -export declare type Compiler = (message: string) => MFunc; -export interface Replacements { - [s: string]: any; -} -export interface Messages { - [s: string]: { - [s: string]: string | MFunc; - }; -} -export interface SetupOptions { - messages?: Messages; - locale?: string; - idGenerator?: (message: string) => string; - compiler?: Compiler; -} -export interface PluralOptions { - other: string; - one?: string; - zero?: string; -} -export interface TFunc { - (message: string, replacements?: Replacements, id?: string): string; - lookup?: (id: string, replacements?: Replacements, defaultMessage?: string) => string; - setup?: (options?: SetupOptions) => any; - date?: (value: any, formatName?: string, locale?: string) => string; - number?: (value: any, formatName?: string, locale?: string) => string; - $?: (message: string, replacements?: any, id?: string) => any[]; - _i18nInstance?: I18n; -} -export declare type IntlFormat = Intl.DateTimeFormat | Intl.NumberFormat; -export declare type IntlFormatType = (typeof Intl.DateTimeFormat | typeof Intl.NumberFormat); -export declare type IntlFormatOptions = Intl.DateTimeFormatOptions | Intl.NumberFormatOptions; -export declare type CachedFormatter = (locale: string, formatOptions?: IntlFormatOptions) => IntlFormat; -export declare let dateTimeFormatOptions: { - [s: string]: Intl.DateTimeFormatOptions; -}; -export declare let numberFormatOptions: { - [s: string]: Intl.NumberFormatOptions; -}; -export declare const collationTable: string[]; -export declare const defaultLanguage = "en"; -export declare const generator: { - plain(message: string): string; - hyphens(message: string): string; -}; -export declare function Plural(pluralizeFor: string, options: PluralOptions): string; -export declare function createCachedFormatter(intlFormat: (IntlFormatType)): CachedFormatter; -export declare class I18n { - locale: string; - messages: Messages; - idGenerator: (message: string) => any; - dateFormatter: CachedFormatter; - numberFormatter: CachedFormatter; - compiler: Compiler; - static defaultSetup: SetupOptions; - constructor(options?: SetupOptions); - format(type: string, value: number | Date, formatStyle?: string, locale?: string): any; - getKey(key: string, locale?: string): MFunc | String; - generateId(message: string): any; - lookup(id: string, replacements?: Replacements, defaultMessage?: string): string; - setup(options?: SetupOptions): SetupOptions; -} -export declare function i18nNamespace(): TFunc; -export declare const T: TFunc; -export declare const makeI18n: typeof i18nNamespace; +export { Plural, generator } from "./helpers"; +export { default as T, makeT } from "./t-i18n"; diff --git a/dist/es6/index.js b/dist/es6/index.js index 67b9290..428120a 100644 --- a/dist/es6/index.js +++ b/dist/es6/index.js @@ -1,209 +1,2 @@ -/// -export let dateTimeFormatOptions = { - short: { - month: 'short', - day: 'numeric', - year: "numeric" - }, - long: { - month: 'long', - day: 'numeric', - year: 'numeric' - }, - dateTime: { - month: 'short', - day: 'numeric', - year: 'numeric', - hour: 'numeric', - minute: 'numeric' - } -}; -dateTimeFormatOptions.default = dateTimeFormatOptions.long; -export let numberFormatOptions = { - currency: { - style: 'currency', - currency: "USD" - }, - decimal: { - style: 'decimal' - }, - percent: { - style: 'percent' - } -}; -numberFormatOptions.default = numberFormatOptions.decimal; -export const collationTable = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ#'.split(''); -export const defaultLanguage = "en"; -const spaceRegex = /\s/g; -const nonWordRegex = /\W/g; -export const generator = { - plain(message) { - return message; - }, - hyphens(message) { - const hyphenated = message.toLowerCase().trim().replace(spaceRegex, "-").replace(nonWordRegex, '-'); - return hyphenated; - } -}; -// helper function to produce a pluralized string -export function Plural(pluralizeFor, options) { - const { zero, one, other } = options; - return "{" + pluralizeFor + ", plural,\n" + - (zero ? "\t=0{" + zero + "}\n" : "") + - (one ? "\tone{" + one + "}\n" : "") + - ("\tother{" + other + "}}"); -} -// Create cached versions of Intl.DateTimeFormat and Intl.NumberFormat -export function createCachedFormatter(intlFormat) { - let cache = {}; - return function (locale, formatOptions) { - const args = Array.prototype.slice.call(arguments); - const id = locale + "-" + JSON.stringify(formatOptions); - if (id in cache) - return cache[id]; - const formatter = new (Function.prototype.bind.call(intlFormat, null, ...args)); - cache[id] = formatter; - return formatter; - }; -} -export class I18n { - constructor(options = null) { - this.setup(Object.assign({}, I18n.defaultSetup, options)); - this.dateFormatter = createCachedFormatter(Intl.DateTimeFormat); - this.numberFormatter = createCachedFormatter(Intl.NumberFormat); - } - format(type, value, formatStyle, locale = this.locale) { - const options = (type === "date") ? dateTimeFormatOptions : numberFormatOptions; - const formatter = (type === "date") ? this.dateFormatter : this.numberFormatter; - return formatter.call(this, locale, (options[formatStyle] || options.default)).format(value); - } - getKey(key, locale = this.locale) { - const messages = this.messages[locale]; - const defaultMessages = this.messages[defaultLanguage]; - if (messages && messages[key]) { - return messages[key]; - } - if (defaultMessages && defaultMessages[key]) { - return defaultMessages[key]; - } - return; - } - generateId(message) { - return this.idGenerator(message); - } - lookup(id, replacements = null, defaultMessage = null) { - const translation = this.getKey(id, this.locale) || defaultMessage || id; - if (typeof translation === "string") { - if (this.compiler) - return this.compiler(translation)(replacements); - return compileICU(translation, replacements); - } - return translation(replacements); - } - setup(options = {}) { - const { locale, idGenerator, messages, compiler } = options; - if (idGenerator) - this.idGenerator = idGenerator; - if (locale) - this.locale = locale; - if (messages) - this.messages = messages; - if (compiler) - this.compiler = compiler; - return { - messages: this.messages, - locale: this.locale, - idGenerator: this.idGenerator, - compiler: this.compiler - }; - } -} -I18n.defaultSetup = { - messages: {}, - locale: defaultLanguage, - idGenerator: generator.hyphens -}; -let parser; -function parseXML(xmlString, replacements) { - if (typeof parser === 'undefined') { - parser = new DOMParser(); - } - const xmlDoc = parser.parseFromString("" + xmlString + "", "text/xml"); - const firstNode = xmlDoc.firstChild.firstChild; - if (firstNode.nodeName === "parseerror") { - throw new Error("Could not parse XML string: " + firstNode.textContent); - } - const nodes = xmlDoc.firstChild.childNodes; - let elements = []; - let factory; - let props; - for (let i = 0; i < nodes.length; i++) { - let { nodeName, nodeType, textContent } = nodes[i]; - if (nodeType === Node.ELEMENT_NODE) { - let reactComponent = replacements[nodeName]; - if (Array.isArray(reactComponent)) { - [factory, props] = reactComponent; - elements.push(factory(props, textContent)); - } - else { - elements.push(reactComponent); - } - } - else { - elements.push(textContent); - } - } - return elements; -} -const TOKEN = { - START: '{', - END: '}', -}; -function compileICU(icuString, replacements) { - if (!replacements) - return icuString; - let currentElement = ''; - let parsedElements = []; - for (let i = 0; i < icuString.length; i++) { - switch (icuString.charAt(i)) { - case TOKEN.START: - parsedElements.push(currentElement); - currentElement = ''; - break; - case TOKEN.END: - parsedElements.push(replacements[currentElement]); - currentElement = ''; - break; - default: - currentElement += icuString.charAt(i); - } - } - parsedElements.push(currentElement); - return parsedElements.join(''); -} -function createT(context) { - let T = function translate(message, replacements, id) { - if (!id) - id = context.generateId(message); - return context.lookup(id, replacements, message); - }; - T._i18nInstance = context; - T.setup = context.setup.bind(T._i18nInstance); - T.lookup = context.lookup.bind(T._i18nInstance); - T.date = context.format.bind(T._i18nInstance, "date"); - T.number = context.format.bind(T._i18nInstance, "number"); - T.$ = function translateReact(message, replacements, id) { - const translatedMessage = T.apply(this, arguments); - const reactElements = parseXML(translatedMessage, replacements); - return reactElements; - }; - return T; -} -export function i18nNamespace() { - let i18nInstance = new I18n(); - return createT(i18nInstance); -} -// singleton -export const T = i18nNamespace(); -// or roll your own -export const makeI18n = i18nNamespace; +export { Plural, generator } from "./helpers"; +export { default as T, makeT } from "./t-i18n"; diff --git a/dist/index.d.ts b/dist/index.d.ts index c07e420..428120a 100644 --- a/dist/index.d.ts +++ b/dist/index.d.ts @@ -1,77 +1,2 @@ -/// -/** - * T.js - lightweight localization - * v0.2 - * - * T.js defers to standards to do the hard work of localization. The browser Intl API is use to format - * dates and numbers. Messages are provided as functions rather than strings, so they can be compiled at build time. - * - * - * Cobbled together by Mitch Cohen on July 31, 2017 - */ -export declare type MFunc = (replacements?: Replacements) => string; -export declare type Compiler = (message: string) => MFunc; -export interface Replacements { - [s: string]: any; -} -export interface Messages { - [s: string]: { - [s: string]: string | MFunc; - }; -} -export interface SetupOptions { - messages?: Messages; - locale?: string; - idGenerator?: (message: string) => string; - compiler?: Compiler; -} -export interface PluralOptions { - other: string; - one?: string; - zero?: string; -} -export interface TFunc { - (message: string, replacements?: Replacements, id?: string): string; - lookup?: (id: string, replacements?: Replacements, defaultMessage?: string) => string; - setup?: (options?: SetupOptions) => any; - date?: (value: any, formatName?: string, locale?: string) => string; - number?: (value: any, formatName?: string, locale?: string) => string; - $?: (message: string, replacements?: any, id?: string) => any[]; - _i18nInstance?: I18n; -} -export declare type IntlFormat = Intl.DateTimeFormat | Intl.NumberFormat; -export declare type IntlFormatType = (typeof Intl.DateTimeFormat | typeof Intl.NumberFormat); -export declare type IntlFormatOptions = Intl.DateTimeFormatOptions | Intl.NumberFormatOptions; -export declare type CachedFormatter = (locale: string, formatOptions?: IntlFormatOptions) => IntlFormat; -export declare let dateTimeFormatOptions: { - [s: string]: Intl.DateTimeFormatOptions; -}; -export declare let numberFormatOptions: { - [s: string]: Intl.NumberFormatOptions; -}; -export declare const collationTable: string[]; -export declare const defaultLanguage = "en"; -export declare const generator: { - plain(message: string): string; - hyphens(message: string): string; -}; -export declare function Plural(pluralizeFor: string, options: PluralOptions): string; -export declare function createCachedFormatter(intlFormat: (IntlFormatType)): CachedFormatter; -export declare class I18n { - locale: string; - messages: Messages; - idGenerator: (message: string) => any; - dateFormatter: CachedFormatter; - numberFormatter: CachedFormatter; - compiler: Compiler; - static defaultSetup: SetupOptions; - constructor(options?: SetupOptions); - format(type: string, value: number | Date, formatStyle?: string, locale?: string): any; - getKey(key: string, locale?: string): MFunc | String; - generateId(message: string): any; - lookup(id: string, replacements?: Replacements, defaultMessage?: string): string; - setup(options?: SetupOptions): SetupOptions; -} -export declare function i18nNamespace(): TFunc; -export declare const T: TFunc; -export declare const makeI18n: typeof i18nNamespace; +export { Plural, generator } from "./helpers"; +export { default as T, makeT } from "./t-i18n"; diff --git a/dist/index.js b/dist/index.js index a1445e0..e43d5cc 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1,231 +1,8 @@ "use strict"; -/// -var __assign = (this && this.__assign) || Object.assign || function(t) { - for (var s, i = 1, n = arguments.length; i < n; i++) { - s = arguments[i]; - for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) - t[p] = s[p]; - } - return t; -}; Object.defineProperty(exports, "__esModule", { value: true }); -exports.dateTimeFormatOptions = { - short: { - month: 'short', - day: 'numeric', - year: "numeric" - }, - long: { - month: 'long', - day: 'numeric', - year: 'numeric' - }, - dateTime: { - month: 'short', - day: 'numeric', - year: 'numeric', - hour: 'numeric', - minute: 'numeric' - } -}; -exports.dateTimeFormatOptions.default = exports.dateTimeFormatOptions.long; -exports.numberFormatOptions = { - currency: { - style: 'currency', - currency: "USD" - }, - decimal: { - style: 'decimal' - }, - percent: { - style: 'percent' - } -}; -exports.numberFormatOptions.default = exports.numberFormatOptions.decimal; -exports.collationTable = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ#'.split(''); -exports.defaultLanguage = "en"; -var spaceRegex = /\s/g; -var nonWordRegex = /\W/g; -exports.generator = { - plain: function (message) { - return message; - }, - hyphens: function (message) { - var hyphenated = message.toLowerCase().trim().replace(spaceRegex, "-").replace(nonWordRegex, '-'); - return hyphenated; - } -}; -// helper function to produce a pluralized string -function Plural(pluralizeFor, options) { - var zero = options.zero, one = options.one, other = options.other; - return "{" + pluralizeFor + ", plural,\n" + - (zero ? "\t=0{" + zero + "}\n" : "") + - (one ? "\tone{" + one + "}\n" : "") + - ("\tother{" + other + "}}"); -} -exports.Plural = Plural; -// Create cached versions of Intl.DateTimeFormat and Intl.NumberFormat -function createCachedFormatter(intlFormat) { - var cache = {}; - return function (locale, formatOptions) { - var args = Array.prototype.slice.call(arguments); - var id = locale + "-" + JSON.stringify(formatOptions); - if (id in cache) - return cache[id]; - var formatter = new ((_a = Function.prototype.bind).call.apply(_a, [intlFormat, null].concat(args))); - cache[id] = formatter; - return formatter; - var _a; - }; -} -exports.createCachedFormatter = createCachedFormatter; -var I18n = (function () { - function I18n(options) { - if (options === void 0) { options = null; } - this.setup(__assign({}, I18n.defaultSetup, options)); - this.dateFormatter = createCachedFormatter(Intl.DateTimeFormat); - this.numberFormatter = createCachedFormatter(Intl.NumberFormat); - } - I18n.prototype.format = function (type, value, formatStyle, locale) { - if (locale === void 0) { locale = this.locale; } - var options = (type === "date") ? exports.dateTimeFormatOptions : exports.numberFormatOptions; - var formatter = (type === "date") ? this.dateFormatter : this.numberFormatter; - return formatter.call(this, locale, (options[formatStyle] || options.default)).format(value); - }; - I18n.prototype.getKey = function (key, locale) { - if (locale === void 0) { locale = this.locale; } - var messages = this.messages[locale]; - var defaultMessages = this.messages[exports.defaultLanguage]; - if (messages && messages[key]) { - return messages[key]; - } - if (defaultMessages && defaultMessages[key]) { - return defaultMessages[key]; - } - return; - }; - I18n.prototype.generateId = function (message) { - return this.idGenerator(message); - }; - I18n.prototype.lookup = function (id, replacements, defaultMessage) { - if (replacements === void 0) { replacements = null; } - if (defaultMessage === void 0) { defaultMessage = null; } - var translation = this.getKey(id, this.locale) || defaultMessage || id; - if (typeof translation === "string") { - if (this.compiler) - return this.compiler(translation)(replacements); - return compileICU(translation, replacements); - } - return translation(replacements); - }; - I18n.prototype.setup = function (options) { - if (options === void 0) { options = {}; } - var locale = options.locale, idGenerator = options.idGenerator, messages = options.messages, compiler = options.compiler; - if (idGenerator) - this.idGenerator = idGenerator; - if (locale) - this.locale = locale; - if (messages) - this.messages = messages; - if (compiler) - this.compiler = compiler; - return { - messages: this.messages, - locale: this.locale, - idGenerator: this.idGenerator, - compiler: this.compiler - }; - }; - I18n.defaultSetup = { - messages: {}, - locale: exports.defaultLanguage, - idGenerator: exports.generator.hyphens - }; - return I18n; -}()); -exports.I18n = I18n; -var parser; -function parseXML(xmlString, replacements) { - if (typeof parser === 'undefined') { - parser = new DOMParser(); - } - var xmlDoc = parser.parseFromString("" + xmlString + "", "text/xml"); - var firstNode = xmlDoc.firstChild.firstChild; - if (firstNode.nodeName === "parseerror") { - throw new Error("Could not parse XML string: " + firstNode.textContent); - } - var nodes = xmlDoc.firstChild.childNodes; - var elements = []; - var factory; - var props; - for (var i = 0; i < nodes.length; i++) { - var _a = nodes[i], nodeName = _a.nodeName, nodeType = _a.nodeType, textContent = _a.textContent; - if (nodeType === Node.ELEMENT_NODE) { - var reactComponent = replacements[nodeName]; - if (Array.isArray(reactComponent)) { - factory = reactComponent[0], props = reactComponent[1]; - elements.push(factory(props, textContent)); - } - else { - elements.push(reactComponent); - } - } - else { - elements.push(textContent); - } - } - return elements; -} -var TOKEN = { - START: '{', - END: '}', -}; -function compileICU(icuString, replacements) { - if (!replacements) - return icuString; - var currentElement = ''; - var parsedElements = []; - for (var i = 0; i < icuString.length; i++) { - switch (icuString.charAt(i)) { - case TOKEN.START: - parsedElements.push(currentElement); - currentElement = ''; - break; - case TOKEN.END: - parsedElements.push(replacements[currentElement]); - currentElement = ''; - break; - default: - currentElement += icuString.charAt(i); - } - } - parsedElements.push(currentElement); - return parsedElements.join(''); -} -function createT(context) { - var T = function translate(message, replacements, id) { - if (!id) - id = context.generateId(message); - return context.lookup(id, replacements, message); - }; - T._i18nInstance = context; - T.setup = context.setup.bind(T._i18nInstance); - T.lookup = context.lookup.bind(T._i18nInstance); - T.date = context.format.bind(T._i18nInstance, "date"); - T.number = context.format.bind(T._i18nInstance, "number"); - T.$ = function translateReact(message, replacements, id) { - var translatedMessage = T.apply(this, arguments); - var reactElements = parseXML(translatedMessage, replacements); - return reactElements; - }; - return T; -} -function i18nNamespace() { - var i18nInstance = new I18n(); - return createT(i18nInstance); -} -exports.i18nNamespace = i18nNamespace; -// singleton -exports.T = i18nNamespace(); -// or roll your own -exports.makeI18n = i18nNamespace; +var helpers_1 = require("./helpers"); +exports.Plural = helpers_1.Plural; +exports.generator = helpers_1.generator; +var t_i18n_1 = require("./t-i18n"); +exports.T = t_i18n_1.default; +exports.makeT = t_i18n_1.makeT; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..413eb56 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,520 @@ +{ + "name": "t-i18n", + "version": "0.2.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@types/chai": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.0.4.tgz", + "integrity": "sha512-cvU0HomQ7/aGDQJZsbtJXqBQ7w4J4TqLB0Z/h8mKrpRjfeZEvTbygkfJEb7fWdmwpIeDeFmIVwAEqS0OYuUv3Q==", + "dev": true + }, + "@types/mocha": { + "version": "2.2.42", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-2.2.42.tgz", + "integrity": "sha512-b6gVDoxEbAQGwbV7gSzeFw/hy3/eEAokztktdzl4bHvGgb9K5zW4mVQDlVYch2w31m8t/J7L2iqhQvz3r5edCQ==", + "dev": true + }, + "@types/node": { + "version": "8.0.24", + "resolved": "https://registry.npmjs.org/@types/node/-/node-8.0.24.tgz", + "integrity": "sha512-c3Npme+2JGqxW8+B+aXdN5SPIlCf1C8WxQC6Ea39rO/ASPosnMkWVR16mDJtRE+2dr2xwOQ7DiLxb+wO/TWuPg==", + "dev": true + }, + "@types/react": { + "version": "16.0.5", + "resolved": "https://registry.npmjs.org/@types/react/-/react-16.0.5.tgz", + "integrity": "sha512-Wo/JT6Cpl7XuLA1Ov2M2Rso4Tep7rX6h1csbqhNDaSxqeY8nxUbrDkT6vJrKVu+7tw7vmJP9libZSReV9GsG9A==" + }, + "ansi-styles": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", + "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "dev": true, + "requires": { + "color-convert": "1.9.0" + } + }, + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "dev": true + }, + "asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=", + "dev": true + }, + "assertion-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.0.2.tgz", + "integrity": "sha1-E8pRXYYgbaC6xm6DTdOX2HWBCUw=", + "dev": true + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "brace-expansion": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", + "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", + "dev": true, + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "chai": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.1.1.tgz", + "integrity": "sha1-ZuISeebzxkFf+CMYeCJ5AOIXGzk=", + "dev": true, + "requires": { + "assertion-error": "1.0.2", + "check-error": "1.0.2", + "deep-eql": "2.0.2", + "get-func-name": "2.0.0", + "pathval": "1.1.0", + "type-detect": "4.0.3" + } + }, + "chalk": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.1.0.tgz", + "integrity": "sha512-LUHGS/dge4ujbXMJrnihYMcL4AoOweGnw9Tp3kQuqy1Kx5c1qKjqvMJZ6nVJPMWJtKCTN72ZogH3oeSO9g9rXQ==", + "dev": true, + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "4.4.0" + } + }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "dev": true + }, + "color-convert": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.0.tgz", + "integrity": "sha1-Gsz5fdc5uYO/mU1W/sj5WFNkG3o=", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "core-js": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz", + "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=", + "dev": true + }, + "create-react-class": { + "version": "15.6.0", + "resolved": "https://registry.npmjs.org/create-react-class/-/create-react-class-15.6.0.tgz", + "integrity": "sha1-q0SEl8JlZuHilBPogyB9V8/nvtQ=", + "dev": true, + "requires": { + "fbjs": "0.8.14", + "loose-envify": "1.3.1", + "object-assign": "4.1.1" + } + }, + "deep-eql": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-2.0.2.tgz", + "integrity": "sha1-sbrAblbwp2d3aG1Qyf63XC7XZ5o=", + "dev": true, + "requires": { + "type-detect": "3.0.0" + }, + "dependencies": { + "type-detect": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-3.0.0.tgz", + "integrity": "sha1-RtDMhVOrt7E6NSsNbeov1Y8tm1U=", + "dev": true + } + } + }, + "diff": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.3.1.tgz", + "integrity": "sha512-MKPHZDMB0o6yHyDryUOScqZibp914ksXwAMYMTHj6KO8UeKsRYNJD3oNCKjTqZon+V488P7N/HzXF8t7ZR95ww==", + "dev": true + }, + "encoding": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", + "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", + "dev": true, + "requires": { + "iconv-lite": "0.4.18" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "fbjs": { + "version": "0.8.14", + "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.14.tgz", + "integrity": "sha1-0dviviVMNakeCfMfnNUKQLKg7Rw=", + "dev": true, + "requires": { + "core-js": "1.2.7", + "isomorphic-fetch": "2.2.1", + "loose-envify": "1.3.1", + "object-assign": "4.1.1", + "promise": "7.3.1", + "setimmediate": "1.0.5", + "ua-parser-js": "0.7.14" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "dev": true + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "has-flag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", + "dev": true + }, + "iconv-lite": { + "version": "0.4.18", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.18.tgz", + "integrity": "sha512-sr1ZQph3UwHTR0XftSbK85OvBbxe/abLGzEnPENCQwmHf7sck8Oyu4ob3LgBxWWxRoM+QszeUyl7jbqapu2TqA==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true + }, + "isomorphic-fetch": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz", + "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=", + "dev": true, + "requires": { + "node-fetch": "1.7.2", + "whatwg-fetch": "2.0.3" + } + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", + "dev": true + }, + "loose-envify": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", + "integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=", + "dev": true, + "requires": { + "js-tokens": "3.0.2" + } + }, + "make-error": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.0.tgz", + "integrity": "sha1-Uq06M5zPEM5itAQLcI/nByRLi5Y=", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "1.1.8" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + } + } + }, + "node-fetch": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.2.tgz", + "integrity": "sha512-xZZUq2yDhKMIn/UgG5q//IZSNLJIwW2QxS14CNH5spuiXkITM2pUitjdq58yLSaU7m4M0wBNaM2Gh/ggY4YJig==", + "dev": true, + "requires": { + "encoding": "0.1.12", + "is-stream": "1.1.0" + } + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "pathval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", + "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", + "dev": true + }, + "promise": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", + "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "dev": true, + "requires": { + "asap": "2.0.6" + } + }, + "prop-types": { + "version": "15.5.10", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.5.10.tgz", + "integrity": "sha1-J5ffwxJhguOpXj37suiT3ddFYVQ=", + "dev": true, + "requires": { + "fbjs": "0.8.14", + "loose-envify": "1.3.1" + } + }, + "react": { + "version": "15.6.1", + "resolved": "https://registry.npmjs.org/react/-/react-15.6.1.tgz", + "integrity": "sha1-uqhDTsZ4C96ZfNw4C3nNM7ljk98=", + "dev": true, + "requires": { + "create-react-class": "15.6.0", + "fbjs": "0.8.14", + "loose-envify": "1.3.1", + "object-assign": "4.1.1", + "prop-types": "15.5.10" + } + }, + "react-dom-factories": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/react-dom-factories/-/react-dom-factories-1.0.1.tgz", + "integrity": "sha1-xQaSrF/xrbOdht/m2+NIXaz1hFU=", + "dev": true + }, + "setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", + "dev": true + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + }, + "source-map-support": { + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.17.tgz", + "integrity": "sha512-30c1Ch8FSjV0FwC253iftbbj0dU/OXoSg1LAEGZJUlGgjTNj6cu+DVqJWWIZJY5RXLWV4eFtR+4ouo0VIOYOTg==", + "dev": true, + "requires": { + "source-map": "0.5.7" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true + }, + "supports-color": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.4.0.tgz", + "integrity": "sha512-rKC3+DyXWgK0ZLKwmRsrkyHVZAjNkfzeehuFWdGGcqGDTZFH73+RH6S/RDAAxl9GusSjZSUWYLmT9N5pzXFOXQ==", + "dev": true, + "requires": { + "has-flag": "2.0.0" + } + }, + "ts-node": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-3.3.0.tgz", + "integrity": "sha1-wTxqMCTjC+EYDdUwOPwgkonUv2k=", + "dev": true, + "requires": { + "arrify": "1.0.1", + "chalk": "2.1.0", + "diff": "3.3.1", + "make-error": "1.3.0", + "minimist": "1.2.0", + "mkdirp": "0.5.1", + "source-map-support": "0.4.17", + "tsconfig": "6.0.0", + "v8flags": "3.0.0", + "yn": "2.0.0" + } + }, + "tsconfig": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-6.0.0.tgz", + "integrity": "sha1-aw6DdgA9evGGT434+J3QBZ/80DI=", + "dev": true, + "requires": { + "strip-bom": "3.0.0", + "strip-json-comments": "2.0.1" + } + }, + "type-detect": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.3.tgz", + "integrity": "sha1-Dj8mcLRAmbC0bChNE2p+9Jx0wuo=", + "dev": true + }, + "typescript": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.5.2.tgz", + "integrity": "sha1-A4qV99m7tCCxvzW6MdTFwd0//jQ=", + "dev": true + }, + "ua-parser-js": { + "version": "0.7.14", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.14.tgz", + "integrity": "sha1-EQ1T+kw/MmwSEpK76skE0uAzh8o=", + "dev": true + }, + "user-home": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/user-home/-/user-home-1.1.1.tgz", + "integrity": "sha1-K1viOjK2Onyd640PKNSFcko98ZA=", + "dev": true + }, + "v8flags": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.0.0.tgz", + "integrity": "sha512-AGl+C+4qpeSu2g3JxCD/mGFFOs/vVZ3XREkD3ibQXEqr4Y4zgIrPWW124/IKJFHOIVFIoH8miWrLf0o84HYjwA==", + "dev": true, + "requires": { + "user-home": "1.1.1" + } + }, + "whatwg-fetch": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz", + "integrity": "sha1-nITsLc9oGH/wC8ZOEnS0QhduHIQ=", + "dev": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "xmldom": { + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.27.tgz", + "integrity": "sha1-1QH5ezvbQDr4757MIFcxh6rawOk=", + "dev": true + }, + "yn": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yn/-/yn-2.0.0.tgz", + "integrity": "sha1-5a2ryKz0CPY4X8dklWhMiOavaJo=", + "dev": true + } + } +} diff --git a/package.json b/package.json index 3b3c82e..960b187 100644 --- a/package.json +++ b/package.json @@ -1,20 +1,27 @@ { "name": "t-i18n", - "version": "0.2.0-alpha.1", + "version": "0.2.0", "description": "Simple, standards-based localization", + "author": "Mitch Cohen ", + "license": "MIT", "main": "dist/index.js", "types": "dist/index.d.ts", "scripts": { - "build": "cd src; tsc -d; tsc -d -p tsconfig.es6.json; cd ../scripts; tsc", - "test": "mocha -r ts-node/register test/*.test.ts" + "clean": "cd dist/; rm -rf *", + "build": "npm run clean; cd src/; tsc -d; tsc -d -p tsconfig.es6.json; cd ../scripts; tsc", + "prepublish": "npm run build", + "test": "mocha -r ts-node/register ./test/*.test.ts" }, "bin": { "extract-strings": "./scripts/extract-strings.js" }, "keywords": [ - "i18n" + "i18n", + "l10n", + "globalization", + "translation", + "icu" ], - "author": "Mitch Cohen", "devDependencies": { "@types/chai": "^4.0.4", "@types/mocha": "^2.2.42", @@ -24,7 +31,7 @@ "react": "^15.6.1", "react-dom-factories": "^1.0.1", "ts-node": "^3.3.0", - "typescript": "^2.4.2", + "typescript": "^2.5.2", "xmldom": "^0.1.27" }, "dependencies": { diff --git a/src/format.ts b/src/format.ts new file mode 100644 index 0000000..3178110 --- /dev/null +++ b/src/format.ts @@ -0,0 +1,58 @@ +// Format functions accept both date and number formatters +export type IntlFormat = Intl.DateTimeFormat|Intl.NumberFormat; +export type IntlFormatType = (typeof Intl.DateTimeFormat | typeof Intl.NumberFormat ) +export type IntlFormatOptions = Intl.DateTimeFormatOptions | Intl.NumberFormatOptions; +export type CachedFormatter = (locale: string, formatOptions?: IntlFormatOptions) => IntlFormat; + +export let dateTimeFormatOptions: {[s: string]: Intl.DateTimeFormatOptions} = { + short: { + month: 'short', + day: 'numeric', + year: "numeric" + }, + long: { + month: 'long', + day: 'numeric', + year: 'numeric' + }, + dateTime: { + month: 'short', + day: 'numeric', + year: 'numeric', + hour: 'numeric', + minute: 'numeric' + } +}; +dateTimeFormatOptions.default = dateTimeFormatOptions.long; + +export let numberFormatOptions: {[s: string]: Intl.NumberFormatOptions} = { + currency: { + style: 'currency', + currency: "USD" + }, + decimal: { + style: 'decimal' + }, + percent: { + style: 'percent' + } +} +numberFormatOptions.default = numberFormatOptions.decimal; + +// Create cached versions of Intl.DateTimeFormat and Intl.NumberFormat +export default function createCachedFormatter(intlFormat: (IntlFormatType)): CachedFormatter { + let cache:any = {}; + + return function (locale: string, formatOptions?: IntlFormatOptions): IntlFormat { + const args = Array.prototype.slice.call(arguments); + const id = locale + "-" + JSON.stringify(formatOptions); + if (id in cache) return cache[id]; + + const formatter: IntlFormat = new (Function.prototype.bind.call( + intlFormat, null, ...args + )); + + cache[id] = formatter; + return formatter; + } +} \ No newline at end of file diff --git a/src/helpers.ts b/src/helpers.ts new file mode 100644 index 0000000..cf88998 --- /dev/null +++ b/src/helpers.ts @@ -0,0 +1,26 @@ +export interface PluralOptions { + other: string; + one?: string; + zero?: string; +} + +const spaceRegex = /\s/g; +const nonWordRegex = /\W/g; + +export const generator = { + plain(message: string): string { + return message; + }, + hyphens(message: string):string { + const hyphenated = message.toLowerCase().trim().replace(spaceRegex, "-").replace(nonWordRegex, '-'); + return hyphenated; + } +} + +export function Plural(pluralizeFor: string, options: PluralOptions): string { + const {zero, one, other} = options; + return "{" + pluralizeFor + ", plural,\n" + + (zero ? "\t=0{" + zero + "}\n" : "") + + (one ? "\tone{" + one + "}\n" : "") + + ("\tother{" + other + "}}"); +} \ No newline at end of file diff --git a/src/icu.ts b/src/icu.ts new file mode 100644 index 0000000..728b395 --- /dev/null +++ b/src/icu.ts @@ -0,0 +1,35 @@ +import { Replacements, ReactReplacements } from "./types"; + +const TOKEN = { + OPEN: '{', + CLOSE: '}', +} + +// ICU grammar (basic value replacement only) +// +// message: (STRING | replacement)* +// replacement: OPEN value CLOSE +// value: STRING + +export default function parseICU(icuString: string, replacements: Replacements|ReactReplacements):string { + if (!replacements) return icuString; + + let currentToken = ''; + let elements = []; + for (let i = 0; i < icuString.length; i++) { + switch (icuString.charAt(i)) { + case TOKEN.OPEN: + elements.push(currentToken); + currentToken = ''; + break; + case TOKEN.CLOSE: + elements.push(replacements[currentToken]); + currentToken = ''; + break; + default: + currentToken += icuString.charAt(i); + } + } + elements.push(currentToken); + return elements.join(''); +} diff --git a/src/index.ts b/src/index.ts index 7795582..c5bc200 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,315 +1,6 @@ -/// +export { Plural, generator } from "./helpers"; -/** - * T.js - lightweight localization - * v0.2 - * - * T.js defers to standards to do the hard work of localization. The browser Intl API is use to format - * dates and numbers. Messages are provided as functions rather than strings, so they can be compiled at build time. - * - * - * Cobbled together by Mitch Cohen on July 31, 2017 - */ - -// Messages functions have replacement/pluralization logic baked in -// Use a tool like MessageFormat to generate these from strings -// -// const mFunc = (name) => "Hello, " + name -// mFunc("Mitch") // "Hello, Mitch" -// -export type MFunc = (replacements?: Replacements) => string; -// Function to compile strings into message functions during runtime -export type Compiler = (message: string) => MFunc; - -// { name: "Mitch", age: 10 } -export interface Replacements { - [s: string]: any -} - -// Message functions are grouped by locale and keyed to IDs -// -// { -// en: { -// "greeting": (name) => "Hello, " + name -// } -// } -// -export interface Messages { - [s: string]: {[s: string]: string|MFunc} -} - -export interface SetupOptions { - messages?: Messages; - locale?: string; - idGenerator?: (message: string) => string; - compiler?: Compiler; -} - -export interface PluralOptions { - other: string; - one?: string; - zero?: string; -} - -// T is exported instead of the underlying instance to allow calls to T("Message to translate") -// -export interface TFunc { - (message: string, replacements?: Replacements, id?: string): string; - lookup?: (id: string, replacements?: Replacements, defaultMessage?:string) => string - setup?: (options?: SetupOptions) => any; - date?: (value: any, formatName?: string, locale?: string) => string; - number?: (value: any, formatName?: string, locale?: string) => string; - $?: (message: string, replacements?: any, id?: string) => any[]; - _i18nInstance?: I18n; -} - - -// Format functions accept both date and number formatters -export type IntlFormat = Intl.DateTimeFormat|Intl.NumberFormat; -export type IntlFormatType = (typeof Intl.DateTimeFormat | typeof Intl.NumberFormat ) -export type IntlFormatOptions = Intl.DateTimeFormatOptions | Intl.NumberFormatOptions; -export type CachedFormatter = (locale: string, formatOptions?: IntlFormatOptions) => IntlFormat; - -export let dateTimeFormatOptions: {[s: string]: Intl.DateTimeFormatOptions} = { - short: { - month: 'short', - day: 'numeric', - year: "numeric" - }, - long: { - month: 'long', - day: 'numeric', - year: 'numeric' - }, - dateTime: { - month: 'short', - day: 'numeric', - year: 'numeric', - hour: 'numeric', - minute: 'numeric' - } -}; -dateTimeFormatOptions.default = dateTimeFormatOptions.long; - -export let numberFormatOptions: {[s: string]: Intl.NumberFormatOptions} = { - currency: { - style: 'currency', - currency: "USD" - }, - decimal: { - style: 'decimal' - }, - percent: { - style: 'percent' - } -} -numberFormatOptions.default = numberFormatOptions.decimal; - -export const collationTable = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ#'.split(''); -export const defaultLanguage = "en"; - -const spaceRegex = /\s/g; -const nonWordRegex = /\W/g; - -export const generator = { - plain(message: string): string { - return message; - }, - hyphens(message: string):string { - const hyphenated = message.toLowerCase().trim().replace(spaceRegex, "-").replace(nonWordRegex, '-'); - return hyphenated; - } -} - -// helper function to produce a pluralized string -export function Plural(pluralizeFor: string, options: PluralOptions): string { - const {zero, one, other} = options; - return "{" + pluralizeFor + ", plural,\n" + - (zero ? "\t=0{" + zero + "}\n" : "") + - (one ? "\tone{" + one + "}\n" : "") + - ("\tother{" + other + "}}"); -} - -// Create cached versions of Intl.DateTimeFormat and Intl.NumberFormat -export function createCachedFormatter(intlFormat: (IntlFormatType)): CachedFormatter { - let cache:any = {}; - - return function (locale: string, formatOptions?: IntlFormatOptions): IntlFormat { - const args = Array.prototype.slice.call(arguments); - const id = locale + "-" + JSON.stringify(formatOptions); - if (id in cache) return cache[id]; - - const formatter: IntlFormat = new (Function.prototype.bind.call( - intlFormat, null, ...args - )); - - cache[id] = formatter; - return formatter; - } -} - -export class I18n { - locale: string; - messages: Messages; - idGenerator: (message: string) => any; - dateFormatter: CachedFormatter; - numberFormatter: CachedFormatter; - compiler: Compiler; - - static defaultSetup: SetupOptions = { - messages: {}, - locale: defaultLanguage, - idGenerator: generator.hyphens - } - - constructor(options: SetupOptions = null) { - this.setup({...I18n.defaultSetup, ...options}); - - this.dateFormatter = createCachedFormatter(Intl.DateTimeFormat); - this.numberFormatter= createCachedFormatter(Intl.NumberFormat); - } - - format(type: string, value: number|Date, formatStyle?: string, locale: string = this.locale) { - const options = (type === "date") ? dateTimeFormatOptions : numberFormatOptions; - const formatter = (type === "date") ? this.dateFormatter : this.numberFormatter; - - return formatter.call(this, locale, (options[formatStyle] || options.default)).format(value); - } - - getKey(key: string, locale: string = this.locale):MFunc|String { - const messages = this.messages[locale]; - const defaultMessages = this.messages[defaultLanguage]; - - if (messages && messages[key]) { - return messages[key]; - } - if (defaultMessages && defaultMessages[key]) { - return defaultMessages[key]; - } - - return; - } - - generateId(message: string) { - return this.idGenerator(message); - } - - lookup(id: string, replacements: Replacements = null, defaultMessage:string = null):string { - const translation = this.getKey(id, this.locale) || defaultMessage || id; - if (typeof translation === "string") { - if (this.compiler) return this.compiler(translation)(replacements); - return compileICU(translation, replacements); - } - - return (translation)(replacements); - } - - setup(options: SetupOptions = {}): SetupOptions { - const {locale, idGenerator, messages, compiler} = options; - if (idGenerator) this.idGenerator = idGenerator; - if (locale) this.locale = locale; - if (messages) this.messages = messages; - if (compiler) this.compiler = compiler; - - return { - messages: this.messages, - locale: this.locale, - idGenerator: this.idGenerator, - compiler: this.compiler - }; - } -} - -let parser: DOMParser; -function parseXML(xmlString: string, replacements: any[]) { - if (typeof parser === 'undefined') { - parser = new DOMParser(); - } - - const xmlDoc = parser.parseFromString("" + xmlString + "", "text/xml"); - const firstNode = xmlDoc.firstChild.firstChild; - if (firstNode.nodeName === "parseerror") { - throw new Error("Could not parse XML string: " + firstNode.textContent) - } - - const nodes = xmlDoc.firstChild.childNodes; - let elements = []; - let factory: React.Factory; - let props: React.Props; - for (let i = 0; i < nodes.length; i++) { - let {nodeName, nodeType, textContent} = nodes[i]; - if (nodeType === Node.ELEMENT_NODE) { - let reactComponent = replacements[nodeName]; - if (Array.isArray(reactComponent)) { - [factory, props] = reactComponent; - elements.push(factory(props, textContent)); - } else { - elements.push(reactComponent); - } - } else { - elements.push(textContent); - } - } - return elements; -} - -const TOKEN = { - START: '{', - END: '}', -} -function compileICU(icuString: string, replacements: Replacements):string { - if (!replacements) return icuString; - - let currentElement = ''; - let parsedElements = []; - for (let i = 0; i < icuString.length; i++) { - switch (icuString.charAt(i)) { - case TOKEN.START: - parsedElements.push(currentElement); - currentElement = ''; - break; - case TOKEN.END: - parsedElements.push(replacements[currentElement]); - currentElement = ''; - break; - default: - currentElement += icuString.charAt(i); - } - } - parsedElements.push(currentElement); - return parsedElements.join(''); -} - -function createT(context: I18n): TFunc { - let T: TFunc = function translate(message: string, replacements?: (Replacements), id?: string): string { - if (!id) id = context.generateId(message); - return context.lookup(id, replacements, message); - } - T._i18nInstance = context; - T.setup = context.setup.bind(T._i18nInstance); - T.lookup = context.lookup.bind(T._i18nInstance); - T.date = context.format.bind(T._i18nInstance, "date"); - T.number = context.format.bind(T._i18nInstance, "number"); - - T.$ = function translateReact(message: string, replacements?: any[], id?: string): React.ReactElement[] { - const translatedMessage = T.apply(this, arguments); - const reactElements = parseXML(translatedMessage, replacements); - return reactElements; - - } - - return T; -} - -export function i18nNamespace(): TFunc { - let i18nInstance: I18n = new I18n(); - - return createT(i18nInstance); - -} - -// singleton -export const T = i18nNamespace(); - -// or roll your own -export const makeI18n = i18nNamespace; \ No newline at end of file +export { + default as T, + makeT +} from "./t-i18n"; diff --git a/src/react.ts b/src/react.ts new file mode 100644 index 0000000..4ad1ca8 --- /dev/null +++ b/src/react.ts @@ -0,0 +1,37 @@ +/// + +import { ReactReplacements, ReactFactory } from "./types"; + +function walk(node: Node, replacements: ReactReplacements): React.ReactNode { + const children = node.childNodes; + // node has no children + if (!children || children.length === 0) { + // node is a self-closing tag or string + return replacements[node.nodeName] || node.nodeValue; + } + // node is a tag with children + const reactChildren: React.ReactNode[] = Array.prototype.slice.call(children).map(child => walk(child, replacements)); + return replaceReactFactory(node.nodeName, reactChildren, replacements) +} + +function replaceReactFactory(name:string, children:React.ReactNode[], replacements: ReactReplacements) { + const [factory, props] = replacements[name] as ReactFactory; + return factory(props, ...children); +} + +let parser: DOMParser; +export default function parseReact(xmlString: string, replacements: ReactReplacements): React.ReactNode[] { + if (typeof parser === 'undefined') { + parser = new DOMParser(); + } + + const xmlDoc = parser.parseFromString("" + xmlString + "", "text/xml"); + if (xmlDoc.firstChild.nodeName === "parseerror") { + throw new Error("Could not parse XML string") + } + + const topLevelElements = Array.prototype.slice.call( + xmlDoc.firstChild.childNodes + ); + return topLevelElements.map(element => walk(element, replacements)) +} \ No newline at end of file diff --git a/src/t-i18n.ts b/src/t-i18n.ts new file mode 100644 index 0000000..1977b17 --- /dev/null +++ b/src/t-i18n.ts @@ -0,0 +1,131 @@ +import { Replacements, Messages, MFunc, SetupOptions, ReactReplacements } from "./types"; +import createCachedFormatter, { CachedFormatter, numberFormatOptions, dateTimeFormatOptions} from "./format"; +import { Plural, generator } from "./helpers"; +import parseICU from "./icu"; +import parseReact from "./react"; + +/** + * T-i18n - lightweight localization + * v0.2 + * + * T-i18n defers to standards to do the hard work of localization. The browser Intl API is use to format + * dates and numbers. Messages are provided as functions rather than strings, so they can be compiled at build time. + * + * + * Cobbled together by Mitch Cohen on July 31, 2017 + */ + +// T is exported instead of the underlying instance to allow calls to T("Message to translate") +// +export interface TFunc { + (message: string, replacements?: Replacements, id?: string): string; + lookup?: (id: string, replacements?: Replacements, defaultMessage?:string) => string + setup?: (options?: SetupOptions) => any; + date?: (value: any, formatName?: string, locale?: string) => string; + number?: (value: any, formatName?: string, locale?: string) => string; + $?: (message: string, replacements?: ReactReplacements, id?: string) => React.ReactNode[]; + _i18nInstance?: I18n; +} + +export const defaultLanguage = "en"; + +export class I18n { + locale: string; + messages: Messages; + idGenerator: (message: string) => any; + dateFormatter: CachedFormatter; + numberFormatter: CachedFormatter; + + static defaultSetup: SetupOptions = { + messages: {}, + locale: defaultLanguage, + idGenerator: generator.hyphens + } + + constructor(options: SetupOptions = null) { + this.setup({...I18n.defaultSetup, ...options}); + + this.dateFormatter = createCachedFormatter(Intl.DateTimeFormat); + this.numberFormatter= createCachedFormatter(Intl.NumberFormat); + } + + format(type: string, value: number|Date, formatStyle?: string, locale: string = this.locale) { + const options = (type === "date") ? dateTimeFormatOptions : numberFormatOptions; + const formatter = (type === "date") ? this.dateFormatter : this.numberFormatter; + + return formatter.call(this, locale, (options[formatStyle] || options.default)).format(value); + } + + getKey(key: string, locale: string = this.locale):MFunc|String { + const messages = this.messages[locale]; + const defaultMessages = this.messages[defaultLanguage]; + + if (messages && messages[key]) { + return messages[key]; + } + if (defaultMessages && defaultMessages[key]) { + return defaultMessages[key]; + } + + return; + } + + generateId(message: string) { + return this.idGenerator(message); + } + + lookup(id: string, replacements: Replacements = null, defaultMessage:string = null):string { + const translation = this.getKey(id, this.locale) || defaultMessage || id; + if (typeof translation === "string") { + return parseICU(translation, replacements); + } + + return (translation)(replacements); + } + + setup(options: SetupOptions = {}): SetupOptions { + const {locale, idGenerator, messages, compiler} = options; + if (idGenerator) this.idGenerator = idGenerator; + if (locale) this.locale = locale; + if (messages) this.messages = messages; + + return { + messages: this.messages, + locale: this.locale, + idGenerator: this.idGenerator + }; + } +} + +function createT(context: I18n): TFunc { + let T: TFunc = function translate(message: string, replacements?: (Replacements), id?: string): string { + if (!id) id = context.generateId(message); + return context.lookup(id, replacements, message); + } + T._i18nInstance = context; + T.setup = context.setup.bind(T._i18nInstance); + T.lookup = context.lookup.bind(T._i18nInstance); + T.date = context.format.bind(T._i18nInstance, "date"); + T.number = context.format.bind(T._i18nInstance, "number"); + + T.$ = function translateReact(message: string, replacements?: ReactReplacements, id?: string): React.ReactNode[] { + const translatedMessage = T.apply(this, arguments); + const reactElements = parseReact(translatedMessage, replacements); + return reactElements; + + } + + return T; +} + +export function i18nNamespace(): TFunc { + let i18nInstance: I18n = new I18n(); + + return createT(i18nInstance); +} + +// singleton (T) +export default i18nNamespace(); + +// or roll your own +export const makeT = i18nNamespace; \ No newline at end of file diff --git a/src/tsconfig.es6.json b/src/tsconfig.es6.json index 0da9d8c..c7547ea 100644 --- a/src/tsconfig.es6.json +++ b/src/tsconfig.es6.json @@ -4,6 +4,8 @@ "module": "es2015", "moduleResolution": "node", "target": "es6", + "removeComments": true, + "declaration": true, "outDir": "../dist/es6/" } } \ No newline at end of file diff --git a/src/tsconfig.json b/src/tsconfig.json index 06cc33d..06c43d8 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -4,6 +4,8 @@ "module": "commonjs", "moduleResolution": "node", "target": "es5", + "removeComments": true, + "declaration": true, "outDir": "../dist/" } } \ No newline at end of file diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..9869319 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,44 @@ +/// + +// Messages functions have replacement/pluralization logic baked in +// Use a tool like MessageFormat to generate these from strings +// +// const mFunc = (name) => "Hello, " + name +// mFunc("Mitch") // "Hello, Mitch" +// +export type MFunc = (replacements?: Replacements) => string; +// Function to compile strings into message functions during runtime +export type Compiler = (message: string) => MFunc; + + + +// Message functions are grouped by locale and keyed to IDs +// +// { +// en: { +// "greeting": (name) => "Hello, " + name +// } +// } +// +export interface Messages { + [s: string]: {[s: string]: string|MFunc} +} + +export interface SetupOptions { + messages?: Messages; + locale?: string; + idGenerator?: (message: string) => string; + compiler?: Compiler; +} + +// { name: "Mitch", age: 10 } +export interface Replacements { + [s: string]: string|number +} + +// React +export type ReactFactory = [React.Factory, React.Props]; + +export interface ReactReplacements { + [s: string]: ReactFactory | React.ReactNode | JSX.Element | string | number +} diff --git a/test/format.test.ts b/test/format.test.ts new file mode 100644 index 0000000..b914b24 --- /dev/null +++ b/test/format.test.ts @@ -0,0 +1,45 @@ +/// + +import { expect } from "chai"; +import createCachedFormatter, {dateTimeFormatOptions, numberFormatOptions} from "../src/format"; + +describe("createCachedFormatter", () => { + it("should create a date formatter", () => { + const options = {day: "numeric", month: "long", year: "numeric"}; + const date = new Date(2017, 8, 1); + const formatter = createCachedFormatter(Intl.DateTimeFormat); + + const expected = new Intl.DateTimeFormat("en", options).format(date); + const result = (formatter("en", options)).format(date); + + expect (result).to.equal(expected); + }); + + it("should cache a new date formatter with the same settings", () => { + const formatter = createCachedFormatter(Intl.DateTimeFormat); + const a = formatter("en", {month: "long", year: "2-digit"}); + const b = formatter("en", {month: "long", year: "2-digit"}); + + const result = (a === b); + + expect (result).to.equal(true); + }); + + it("should create a number formatter", () => { + const formatter = createCachedFormatter(Intl.NumberFormat); + + const expected = new Intl.NumberFormat("es").format(2.3); + const result = (formatter("es")).format(2.3); + expect (result).to.equal(expected); + }); + + it("should cache a new number formatter with the same settings", () => { + const formatter = createCachedFormatter(Intl.NumberFormat); + const a = formatter("ja"); + const b = formatter("ja"); + + const result = (a === b); + + expect (result).to.equal(true); + }); +}); \ No newline at end of file diff --git a/test/i18n.test.ts b/test/t-i18n.test.ts similarity index 73% rename from test/i18n.test.ts rename to test/t-i18n.test.ts index b6c6451..5d9e207 100644 --- a/test/i18n.test.ts +++ b/test/t-i18n.test.ts @@ -1,7 +1,9 @@ /// import { expect } from "chai"; -import {T, makeI18n, createCachedFormatter, Plural, generator, dateTimeFormatOptions, numberFormatOptions} from "../src/index"; +import {T, makeT} from "../src/index"; +import {Plural, generator} from "../src/helpers"; +import {dateTimeFormatOptions, numberFormatOptions} from "../src/format"; import * as DOM from "react-dom-factories"; import * as React from "react"; @@ -30,47 +32,6 @@ describe("Plural", () => { }); }); -describe("createCachedFormatter", () => { - it("should create a date formatter", () => { - const options = {day: "numeric", month: "long", year: "numeric"}; - const date = new Date(2017, 8, 1); - const formatter = createCachedFormatter(Intl.DateTimeFormat); - - const expected = new Intl.DateTimeFormat("en", options).format(date); - const result = (formatter("en", options)).format(date); - - expect (result).to.equal(expected); - }); - - it("should cache a new date formatter with the same settings", () => { - const formatter = createCachedFormatter(Intl.DateTimeFormat); - const a = formatter("en", {month: "long", year: "2-digit"}); - const b = formatter("en", {month: "long", year: "2-digit"}); - - const result = (a === b); - - expect (result).to.equal(true); - }); - - it("should create a number formatter", () => { - const formatter = createCachedFormatter(Intl.NumberFormat); - - const expected = new Intl.NumberFormat("es").format(2.3); - const result = (formatter("es")).format(2.3); - expect (result).to.equal(expected); - }); - - it("should cache a new number formatter with the same settings", () => { - const formatter = createCachedFormatter(Intl.NumberFormat); - const a = formatter("ja"); - const b = formatter("ja"); - - const result = (a === b); - - expect (result).to.equal(true); - }); -}); - describe("T", () => { const T = createI18n(); const messages = { @@ -135,23 +96,23 @@ describe("T.$", () => { const T = createI18n(); const factory = DOM.a; const props = {href: "https://google.com"}; - const element = factory(props, "Hello world"); + const element = factory(props, "world"); it("should replace a react component factory and pass inner string as children", () => { - const expected = element; - const result = T.$("Hello world", { + const expected = ["Hello, ", element, "!"]; + const result = T.$("Hello, world!", { link: [factory, props] }); expect(result).to.be.an('array'); - expect(result[0]).to.deep.equal(expected); + expect(result).to.deep.equal(expected); }); it("should replace a react component", () => { - const expected = element; + const expected = [element]; const result = T.$("", { link: element }) expect(result).to.be.an('array'); - expect(result[0]).to.deep.equal(expected); + expect(result).to.deep.equal(expected); }); }); @@ -218,5 +179,5 @@ describe("T.number", () => { function createI18n() { - return makeI18n(); + return makeT(); } \ No newline at end of file