├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmrc ├── .travis.yml ├── LICENSE ├── examples ├── example-bot.js └── locales │ ├── en-us.json │ ├── en.yaml │ └── ru.yaml ├── lib ├── context.js ├── i18n.js ├── index.d.ts └── pluralize.js ├── package.json ├── readme.md └── test ├── analyse-language-repository.js ├── basics.js └── pluralize.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [*.txt] 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["standard"], 3 | "plugins": ["ava"] 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | node_modules 28 | 29 | # Optional npm cache directory 30 | .npm 31 | 32 | # Optional REPL history 33 | .node_repl_history 34 | package-lock.json 35 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 8 4 | - 9 5 | - 10 6 | - 11 7 | - 12 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Vitaly Domnikov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/example-bot.js: -------------------------------------------------------------------------------- 1 | const Telegraf = require('telegraf') 2 | const path = require('path') 3 | const I18n = require('../lib/i18n') 4 | const { Extra } = Telegraf 5 | 6 | // i18n options 7 | const i18n = new I18n({ 8 | directory: path.resolve(__dirname, 'locales'), 9 | defaultLanguage: 'en', 10 | sessionName: 'session', 11 | useSession: true, 12 | templateData: { 13 | pluralize: I18n.pluralize, 14 | uppercase: (value) => value.toUpperCase() 15 | } 16 | }) 17 | 18 | const bot = new Telegraf(process.env.BOT_TOKEN) 19 | bot.use(Telegraf.session()) 20 | bot.use(i18n.middleware()) 21 | 22 | // Start message handler 23 | bot.start(({ i18n, replyWithHTML }) => replyWithHTML(i18n.t('greeting'))) 24 | 25 | // Using i18n helpers 26 | bot.command('help', I18n.reply('greeting', Extra.HTML())) 27 | 28 | // Set locale to `en` 29 | bot.command('en', ({ i18n, replyWithHTML }) => { 30 | i18n.locale('en-US') 31 | return replyWithHTML(i18n.t('greeting')) 32 | }) 33 | 34 | // Set locale to `ru` 35 | bot.command('ru', ({ i18n, replyWithHTML }) => { 36 | i18n.locale('ru') 37 | return replyWithHTML(i18n.t('greeting')) 38 | }) 39 | 40 | // Add apple to cart 41 | bot.command('add', ({ session, i18n, reply }) => { 42 | session.apples = session.apples || 0 43 | session.apples++ 44 | const message = i18n.t('cart', { apples: session.apples }) 45 | return reply(message) 46 | }) 47 | 48 | // Add apple to cart 49 | bot.command('cart', (ctx) => { 50 | const message = ctx.i18n.t('cart', { apples: ctx.session.apples || 0 }) 51 | return ctx.reply(message) 52 | }) 53 | 54 | // Checkout 55 | bot.command('checkout', ({ reply, i18n }) => reply(i18n.t('checkout'))) 56 | bot.startPolling() 57 | -------------------------------------------------------------------------------- /examples/locales/en-us.json: -------------------------------------------------------------------------------- 1 | { 2 | "checkout": "Yo!" 3 | } 4 | -------------------------------------------------------------------------------- /examples/locales/en.yaml: -------------------------------------------------------------------------------- 1 | greeting: Hello ${uppercase(from.first_name)}! 2 | cart: ${from.first_name}, in your cart ${pluralize(apples, 'apple', 'apples')} 3 | checkout: Thank you 4 | -------------------------------------------------------------------------------- /examples/locales/ru.yaml: -------------------------------------------------------------------------------- 1 | greeting: Привет ${uppercase(from.first_name)}! 2 | cart: В вашей корзине ${pluralize(apples, 'яблоко', 'яблокa', 'яблок')} 3 | -------------------------------------------------------------------------------- /lib/context.js: -------------------------------------------------------------------------------- 1 | class I18nContext { 2 | constructor (repository, config, languageCode, templateData) { 3 | this.repository = repository 4 | this.config = config 5 | this.locale(languageCode || config.defaultLanguage) 6 | this.templateData = { 7 | ...config.templateData, 8 | ...templateData 9 | } 10 | } 11 | 12 | locale (languageCode) { 13 | if (!languageCode) { 14 | return this.languageCode 15 | } 16 | 17 | const code = languageCode.toLowerCase() 18 | const shortCode = code.split('-')[0] 19 | 20 | if (!this.repository[code] && !this.repository[shortCode]) { 21 | this.languageCode = this.config.defaultLanguage 22 | this.shortLanguageCode = this.languageCode.split('-')[0] 23 | return 24 | } 25 | 26 | this.languageCode = code 27 | this.shortLanguageCode = shortCode 28 | } 29 | 30 | getTemplate (languageCode, resourceKey = '') { 31 | return resourceKey 32 | .split('.') 33 | .reduce((acc, key) => acc && acc[key], this.repository[languageCode]) 34 | } 35 | 36 | t (resourceKey, templateData) { 37 | let template = this.getTemplate(this.languageCode, resourceKey) || this.getTemplate(this.shortLanguageCode, resourceKey) 38 | 39 | if (!template && this.config.defaultLanguageOnMissing) { 40 | template = this.getTemplate(this.config.defaultLanguage, resourceKey) 41 | } 42 | 43 | if (!template && this.config.allowMissing) { 44 | template = () => resourceKey 45 | } 46 | 47 | if (!template) { 48 | throw new Error(`telegraf-i18n: '${this.languageCode}.${resourceKey}' not found`) 49 | } 50 | const context = { 51 | ...this.templateData, 52 | ...templateData 53 | } 54 | Object.keys(context) 55 | .filter((key) => typeof context[key] === 'function') 56 | .forEach((key) => (context[key] = context[key].bind(this))) 57 | return template(context) 58 | } 59 | } 60 | 61 | module.exports = I18nContext 62 | -------------------------------------------------------------------------------- /lib/i18n.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const compile = require('compile-template') 3 | const yaml = require('js-yaml') 4 | const path = require('path') 5 | const I18nContext = require('./context.js') 6 | const pluralize = require('./pluralize.js') 7 | 8 | class I18n { 9 | constructor (config = {}) { 10 | this.repository = {} 11 | this.config = { 12 | defaultLanguage: 'en', 13 | sessionName: 'session', 14 | allowMissing: true, 15 | templateData: { 16 | pluralize 17 | }, 18 | ...config 19 | } 20 | if (this.config.directory) { 21 | this.loadLocales(this.config.directory) 22 | } 23 | } 24 | 25 | loadLocales (directory) { 26 | if (!fs.existsSync(directory)) { 27 | throw new Error(`Locales directory '${directory}' not found`) 28 | } 29 | const files = fs.readdirSync(directory) 30 | files.forEach((fileName) => { 31 | const extension = path.extname(fileName) 32 | const languageCode = path.basename(fileName, extension).toLowerCase() 33 | if (extension === '.yaml' || extension === '.yml') { 34 | const data = fs.readFileSync(path.resolve(directory, fileName), 'utf8') 35 | this.loadLocale(languageCode, yaml.safeLoad(data)) 36 | } else if (extension === '.json') { 37 | this.loadLocale(languageCode, require(path.resolve(directory, fileName))) 38 | } 39 | }) 40 | } 41 | 42 | loadLocale (languageCode, i18Data) { 43 | const language = languageCode.toLowerCase() 44 | this.repository[language] = { 45 | ...this.repository[language], 46 | ...compileTemplates(i18Data) 47 | } 48 | } 49 | 50 | resetLocale (languageCode) { 51 | if (languageCode) { 52 | delete this.repository[languageCode.toLowerCase()] 53 | } else { 54 | this.repository = {} 55 | } 56 | } 57 | 58 | availableLocales () { 59 | return Object.keys(this.repository) 60 | } 61 | 62 | resourceKeys (languageCode) { 63 | const language = languageCode.toLowerCase() 64 | return getTemplateKeysRecursive(this.repository[language] || {}) 65 | } 66 | 67 | missingKeys (languageOfInterest, referenceLanguage = this.config.defaultLanguage) { 68 | const interest = this.resourceKeys(languageOfInterest) 69 | const reference = this.resourceKeys(referenceLanguage) 70 | 71 | return reference.filter((ref) => !interest.includes(ref)) 72 | } 73 | 74 | overspecifiedKeys (languageOfInterest, referenceLanguage = this.config.defaultLanguage) { 75 | return this.missingKeys(referenceLanguage, languageOfInterest) 76 | } 77 | 78 | translationProgress (languageOfInterest, referenceLanguage = this.config.defaultLanguage) { 79 | const reference = this.resourceKeys(referenceLanguage).length 80 | const missing = this.missingKeys(languageOfInterest, referenceLanguage).length 81 | 82 | return (reference - missing) / reference 83 | } 84 | 85 | createContext (languageCode, templateData) { 86 | return new I18nContext(this.repository, this.config, languageCode, templateData) 87 | } 88 | 89 | middleware () { 90 | return (ctx, next) => { 91 | const session = this.config.useSession && ctx[this.config.sessionName] 92 | const languageCode = (session && session.__language_code) || (ctx.from && ctx.from.language_code) 93 | 94 | ctx.i18n = new I18nContext( 95 | this.repository, 96 | this.config, 97 | languageCode, 98 | { 99 | from: ctx.from, 100 | chat: ctx.chat 101 | } 102 | ) 103 | 104 | return next().then(() => { 105 | if (session) { 106 | session.__language_code = ctx.i18n.locale() 107 | } 108 | }) 109 | } 110 | } 111 | 112 | t (languageCode, resourceKey, templateData) { 113 | const context = new I18nContext(this.repository, this.config, languageCode, templateData) 114 | return context.t(resourceKey) 115 | } 116 | } 117 | 118 | function compileTemplates (root) { 119 | Object.keys(root).forEach((key) => { 120 | if (!root[key]) { 121 | return 122 | } 123 | if (Array.isArray(root[key])) { 124 | root[key] = root[key].map(compileTemplates) 125 | } 126 | if (typeof root[key] === 'object') { 127 | root[key] = compileTemplates(root[key]) 128 | } 129 | if (typeof root[key] === 'string') { 130 | if (root[key].includes('${')) { 131 | root[key] = compile(root[key]) 132 | } else { 133 | const value = root[key] 134 | root[key] = () => value 135 | } 136 | } 137 | }) 138 | return root 139 | } 140 | 141 | function getTemplateKeysRecursive (root, prefix = '') { 142 | let keys = [] 143 | for (const key of Object.keys(root)) { 144 | const subKey = prefix ? prefix + '.' + key : key 145 | if (typeof root[key] === 'object') { 146 | keys = keys.concat(getTemplateKeysRecursive(root[key], subKey)) 147 | } else { 148 | keys.push(subKey) 149 | } 150 | } 151 | 152 | return keys 153 | } 154 | 155 | I18n.match = function (resourceKey, templateData) { 156 | return (text, ctx) => (text && ctx && ctx.i18n && text === ctx.i18n.t(resourceKey, templateData)) ? [text] : null 157 | } 158 | 159 | I18n.reply = function (resourceKey, extra) { 160 | return ({ reply, i18n }) => reply(i18n.t(resourceKey), extra) 161 | } 162 | 163 | I18n.pluralize = pluralize 164 | module.exports = I18n 165 | -------------------------------------------------------------------------------- /lib/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'telegraf-i18n' { 2 | interface Config { 3 | directory?: string; 4 | useSession?: boolean; 5 | sessionName?: string; 6 | allowMissing?: boolean; 7 | defaultLanguageOnMissing?: boolean; 8 | defaultLanguage?: string; 9 | } 10 | 11 | type ContextUpdate = (ctx: any, next?: (() => any) | undefined) => any; 12 | 13 | export class I18n { 14 | constructor (input: Config); 15 | loadLocales (directory: string): void; 16 | loadLocale (languageCode: string, i18Data: object): void; 17 | resetLocale (languageCode: string): void; 18 | availableLocales (): string[]; 19 | resourceKeys (languageCode: string): string[]; 20 | missingKeys (languageOfInterest: string, referenceLanguage?: string): string[]; 21 | overspecifiedKeys (languageOfInterest: string, referenceLanguage?: string): string[]; 22 | translationProgress (languageOfInterest: string, referenceLanguage?: string): number; 23 | middleware(): ContextUpdate; 24 | createContext (languageCode: string, templateData: object): void; 25 | t (languageCode?: string, resourceKey?: string, templateData?: object): string; 26 | t (resourceKey?: string, templateData?: object): string; 27 | locale (): string; 28 | locale (languageCode?: string): void; 29 | } 30 | 31 | export default I18n; 32 | } 33 | -------------------------------------------------------------------------------- /lib/pluralize.js: -------------------------------------------------------------------------------- 1 | // https://developer.mozilla.org/en-US/docs/Mozilla/Localization/Localization_and_Plurals 2 | 3 | const pluralRules = { 4 | english: (n) => n !== 1 ? 1 : 0, 5 | french: (n) => n > 1 ? 1 : 0, 6 | russian: (n) => { 7 | if (n % 10 === 1 && n % 100 !== 11) { 8 | return 0 9 | } 10 | return n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2 11 | }, 12 | czech: (n) => { 13 | if (n === 1) { 14 | return 0 15 | } 16 | return (n >= 2 && n <= 4) ? 1 : 2 17 | }, 18 | polish: (n) => { 19 | if (n === 1) { 20 | return 0 21 | } 22 | return n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2 23 | }, 24 | icelandic: (n) => (n % 10 !== 1 || n % 100 === 11) ? 1 : 0, 25 | chinese: () => 0, 26 | arabic: (n) => { 27 | if (n >= 0 && n < 3) { 28 | return n 29 | } 30 | if (n % 100 <= 10) { 31 | return 3 32 | } 33 | if (n >= 11 && n % 100 <= 99) { 34 | return 4 35 | } 36 | return 5 37 | } 38 | } 39 | 40 | const mapping = { 41 | english: ['da', 'de', 'en', 'es', 'fi', 'el', 'he', 'hu', 'it', 'nl', 'no', 'pt', 'sv', 'br'], 42 | chinese: ['fa', 'id', 'ja', 'ko', 'lo', 'ms', 'th', 'tr', 'zh', 'jp'], 43 | french: ['fr', 'tl', 'pt-br'], 44 | russian: ['hr', 'ru', 'uk', 'uz'], 45 | czech: ['cs', 'sk'], 46 | icelandic: ['is'], 47 | polish: ['pl'], 48 | arabic: ['ar'] 49 | } 50 | 51 | module.exports = function pluralize (number, ...forms) { 52 | const code = this.shortLanguageCode 53 | let key = Object.keys(mapping).find((key) => mapping[key].includes(code)) 54 | if (!key) { 55 | console.warn(`i18n::Pluralize: Unsupported language ${code}`) 56 | key = 'english' 57 | } 58 | const form = forms[pluralRules[key](number)] 59 | return typeof form === 'function' ? form(number) : `${number} ${form}` 60 | } 61 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "telegraf-i18n", 3 | "version": "6.6.0", 4 | "description": "Telegraf i18n engine", 5 | "main": "lib/i18n.js", 6 | "types": "lib/index.d.ts", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+ssh://git@github.com/telegraf/telegraf-i18n.git" 10 | }, 11 | "keywords": [ 12 | "telegram bot", 13 | "telegraf", 14 | "bot framework", 15 | "i18n", 16 | "internationalization", 17 | "middleware" 18 | ], 19 | "author": "Vitaly Domnikov ", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/telegraf/telegraf-i18n/issues" 23 | }, 24 | "homepage": "https://github.com/telegraf/telegraf-i18n#readme", 25 | "engines": { 26 | "node": ">=8" 27 | }, 28 | "files": [ 29 | "lib" 30 | ], 31 | "scripts": { 32 | "test": "eslint . && ava" 33 | }, 34 | "dependencies": { 35 | "compile-template": "^0.3.1", 36 | "debug": "^4.0.1", 37 | "js-yaml": "^3.6.1" 38 | }, 39 | "peerDependencies": { 40 | "telegraf": "^3.7.1" 41 | }, 42 | "devDependencies": { 43 | "ava": "^2.2.0", 44 | "eslint": "^6.1.0", 45 | "eslint-config-standard": "^13.0.1", 46 | "eslint-plugin-ava": "^7.1.0", 47 | "eslint-plugin-import": "^2.2.0", 48 | "eslint-plugin-node": "^9.1.0", 49 | "eslint-plugin-promise": "^4.0.0", 50 | "eslint-plugin-standard": "^4.0.0", 51 | "telegraf": "^3.7.1" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://img.shields.io/travis/telegraf/telegraf-i18n.svg?branch=master&style=flat-square)](https://travis-ci.org/telegraf/telegraf-i18n) 2 | [![NPM Version](https://img.shields.io/npm/v/telegraf-i18n.svg?style=flat-square)](https://www.npmjs.com/package/telegraf-i18n) 3 | [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square)](http://standardjs.com/) 4 | 5 | # i18n for Telegraf 6 | 7 | Internationalization middleware for [Telegraf](https://github.com/telegraf/telegraf). 8 | 9 | ## Installation 10 | 11 | ```js 12 | $ npm install telegraf-i18n 13 | ``` 14 | 15 | ## Example 16 | 17 | ```js 18 | const Telegraf = require('telegraf') 19 | const TelegrafI18n = require('telegraf-i18n') 20 | 21 | /* 22 | yaml and json are ok 23 | Example directory structure: 24 | ├── locales 25 | │   ├── en.yaml 26 | │   ├── en-US.yaml 27 | │   ├── it.json 28 | │   └── ru.yaml 29 | └── bot.js 30 | */ 31 | 32 | const i18n = new TelegrafI18n({ 33 | defaultLanguage: 'en', 34 | allowMissing: false, // Default true 35 | directory: path.resolve(__dirname, 'locales') 36 | }) 37 | 38 | // Also you can provide i18n data directly 39 | i18n.loadLocale('en', {greeting: 'Hello!'}) 40 | 41 | const app = new Telegraf(process.env.BOT_TOKEN) 42 | 43 | // telegraf-i18n can save current locale setting into session. 44 | const i18n = new TelegrafI18n({ 45 | useSession: true, 46 | defaultLanguageOnMissing: true, // implies allowMissing = true 47 | directory: path.resolve(__dirname, 'locales') 48 | }) 49 | 50 | app.use(Telegraf.memorySession()) 51 | app.use(i18n.middleware()) 52 | 53 | app.hears('/start', (ctx) => { 54 | const message = ctx.i18n.t('greeting', { 55 | username: ctx.from.username 56 | }) 57 | return ctx.reply(message) 58 | }) 59 | 60 | app.startPolling() 61 | ``` 62 | 63 | See full [example](/examples). 64 | 65 | ## User context 66 | 67 | Telegraf user context props and functions: 68 | 69 | ```js 70 | app.use((ctx) => { 71 | ctx.i18n.locale() // Get current locale 72 | ctx.i18n.locale(code) // Set current locale 73 | ctx.i18n.t(resourceKey, [context]) // Get resource value (context will be used by template engine) 74 | }); 75 | ``` 76 | 77 | ## Helpers 78 | 79 | ```js 80 | const { match, reply } = require('telegraf-i18n') 81 | 82 | // In case you use custom keyboard with localized labels. 83 | bot.hears(match('keyboard.foo'), (ctx) => ...) 84 | 85 | //Reply helper 86 | bot.command('help', reply('help')) 87 | ``` 88 | -------------------------------------------------------------------------------- /test/analyse-language-repository.js: -------------------------------------------------------------------------------- 1 | const test = require('ava') 2 | 3 | const I18n = require('../lib/i18n.js') 4 | 5 | test('resourceKeys flat', t => { 6 | const i18n = new I18n() 7 | i18n.loadLocale('en', { 8 | greeting: 'Hello!' 9 | }) 10 | 11 | t.deepEqual(i18n.resourceKeys('en'), [ 12 | 'greeting' 13 | ]) 14 | }) 15 | 16 | test('resourceKeys with depth', t => { 17 | const i18n = new I18n() 18 | i18n.loadLocale('en', { 19 | greeting: 'Hello!', 20 | foo: { 21 | bar: '42', 22 | hell: { 23 | devil: 666 24 | } 25 | } 26 | }) 27 | 28 | t.deepEqual(i18n.resourceKeys('en'), [ 29 | 'greeting', 30 | 'foo.bar', 31 | 'foo.hell.devil' 32 | ]) 33 | }) 34 | 35 | test('resourceKeys of not existing locale are empty', t => { 36 | const i18n = new I18n() 37 | i18n.loadLocale('en', { 38 | greeting: 'Hello!' 39 | }) 40 | 41 | t.deepEqual(i18n.resourceKeys('de'), []) 42 | }) 43 | 44 | function createMultiLanguageExample () { 45 | const i18n = new I18n() 46 | i18n.loadLocale('en', { 47 | greeting: 'Hello!', 48 | checkout: 'Thank you!' 49 | }) 50 | i18n.loadLocale('ru', { 51 | greeting: 'Привет!' 52 | }) 53 | return i18n 54 | } 55 | 56 | test('availableLocales', t => { 57 | const i18n = createMultiLanguageExample() 58 | t.deepEqual(i18n.availableLocales(), [ 59 | 'en', 60 | 'ru' 61 | ]) 62 | }) 63 | 64 | test('missingKeys ', t => { 65 | const i18n = createMultiLanguageExample() 66 | t.deepEqual(i18n.missingKeys('en', 'ru'), []) 67 | t.deepEqual(i18n.missingKeys('ru'), [ 68 | 'checkout' 69 | ]) 70 | }) 71 | 72 | test('overspecifiedKeys', t => { 73 | const i18n = createMultiLanguageExample() 74 | t.deepEqual(i18n.overspecifiedKeys('ru'), []) 75 | t.deepEqual(i18n.overspecifiedKeys('en', 'ru'), [ 76 | 'checkout' 77 | ]) 78 | }) 79 | 80 | test('translationProgress', t => { 81 | const i18n = createMultiLanguageExample() 82 | 83 | // 'checkout' is missing 84 | t.is(i18n.translationProgress('ru'), 0.5) 85 | 86 | // Overspecified (unneeded 'checkout') but everything required is there 87 | t.deepEqual(i18n.translationProgress('en', 'ru'), 1) 88 | }) 89 | -------------------------------------------------------------------------------- /test/basics.js: -------------------------------------------------------------------------------- 1 | const test = require('ava') 2 | 3 | const I18n = require('../lib/i18n.js') 4 | 5 | test('can translate', t => { 6 | const i18n = new I18n() 7 | i18n.loadLocale('en', { 8 | greeting: 'Hello!' 9 | }) 10 | t.is(i18n.t('en', 'greeting'), 'Hello!') 11 | }) 12 | 13 | test('allowMissing false throws', t => { 14 | const i18n = new I18n({ 15 | allowMissing: false 16 | }) 17 | t.throws(() => { 18 | i18n.t('en', 'greeting') 19 | }, 'telegraf-i18n: \'en.greeting\' not found') 20 | }) 21 | -------------------------------------------------------------------------------- /test/pluralize.js: -------------------------------------------------------------------------------- 1 | const test = require('ava') 2 | 3 | const I18n = require('../lib/i18n.js') 4 | 5 | test('can pluralize', t => { 6 | const i18n = new I18n() 7 | i18n.loadLocale('en', { 8 | // eslint-disable-next-line no-template-curly-in-string 9 | pluralize: "${pluralize(n, 'There was an apple', 'There were apples')}" 10 | }) 11 | t.is(i18n.t('en', 'pluralize', { n: 0 }), '0 There were apples') 12 | t.is(i18n.t('en', 'pluralize', { n: 1 }), '1 There was an apple') 13 | t.is(i18n.t('en', 'pluralize', { n: 5 }), '5 There were apples') 14 | }) 15 | 16 | test('can pluralize using functional forms', t => { 17 | const i18n = new I18n() 18 | i18n.loadLocale('en', { 19 | // eslint-disable-next-line no-template-curly-in-string 20 | pluralize: "${pluralize(n, n => 'There was an apple', n => 'There were ' + n + ' apples')}" 21 | }) 22 | t.is(i18n.t('en', 'pluralize', { n: 0 }), 'There were 0 apples') 23 | t.is(i18n.t('en', 'pluralize', { n: 1 }), 'There was an apple') 24 | t.is(i18n.t('en', 'pluralize', { n: 5 }), 'There were 5 apples') 25 | }) 26 | --------------------------------------------------------------------------------