├── .husky └── pre-push ├── .github ├── FUNDING.yml └── workflows │ └── tests.yml ├── tsconfig.json ├── ukti.png ├── src ├── constants.ts ├── index.ts ├── createUktiTranslator.ts ├── renderUktiTemplate.ts └── types.ts ├── .prettierrc.json ├── scripts ├── build-esm.sh └── build-cjs.sh ├── .vscode ├── extensions.json └── settings.json ├── tsconfig.build.cjs.json ├── .editorconfig ├── tsconfig.build.json ├── tsconfig.build.esm.json ├── tsconfig.base.json ├── vite.config.ts ├── .eslintrc.json ├── .gitignore ├── .prettierignore ├── LICENSE ├── package.json ├── tests ├── translator.test-d.ts ├── templates.test.ts └── translator.test.ts ├── .gitattributes └── README.md /.husky/pre-push: -------------------------------------------------------------------------------- 1 | npm test 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: romelperez 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json" 3 | } 4 | -------------------------------------------------------------------------------- /ukti.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romelperez/ukti/HEAD/ukti.png -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const UKTI_LANGUAGE_DEFAULT = 'en' as const 2 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "printWidth": 100 6 | } 7 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types.js' 2 | export * from './constants.js' 3 | export * from './renderUktiTemplate.js' 4 | export * from './createUktiTranslator.js' 5 | -------------------------------------------------------------------------------- /scripts/build-esm.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | mkdir -p ./build/esm 4 | echo "{ \"type\": \"module\" }" >| ./build/esm/package.json 5 | npx tsc -p ./tsconfig.build.esm.json $1 6 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "EditorConfig.EditorConfig", 4 | "esbenp.prettier-vscode", 5 | "dbaeumer.vscode-eslint" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /scripts/build-cjs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | mkdir -p ./build/cjs 4 | echo "{ \"type\": \"commonjs\" }" >| ./build/cjs/package.json 5 | npx tsc -p ./tsconfig.build.cjs.json $1 6 | -------------------------------------------------------------------------------- /tsconfig.build.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.build.json", 3 | "compilerOptions": { 4 | "module": "CommonJS", 5 | "declaration": false, 6 | "outDir": "build/cjs" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "src" 5 | }, 6 | "exclude": ["node_modules", "build", "tests", "vite.config.ts"], 7 | "include": ["src"] 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.build.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.build.json", 3 | "compilerOptions": { 4 | "module": "ESNext", 5 | "declaration": true, 6 | "incremental": true, 7 | "tsBuildInfoFile": ".tsbuildinfo", 8 | "outDir": "build/esm" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ESNext", 4 | "moduleResolution": "node", 5 | "target": "ES2020", 6 | "strict": true, 7 | "skipLibCheck": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "allowSyntheticDefaultImports": true, 10 | "removeComments": true, 11 | "baseUrl": ".", 12 | "rootDir": "." 13 | }, 14 | "exclude": ["node_modules", "build", "vite.config.ts"] 15 | } 16 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { defineConfig } from 'vitest/config' 3 | 4 | export default defineConfig({ 5 | test: { 6 | include: ['tests/**/*.test.ts'] 7 | }, 8 | build: { 9 | lib: { 10 | entry: path.resolve(__dirname, 'src/index.ts'), 11 | formats: ['umd'], 12 | name: 'yrel', 13 | fileName: 'yrel' 14 | }, 15 | rollupOptions: { 16 | external: [], 17 | output: { 18 | dir: path.resolve(__dirname, 'build/umd') 19 | } 20 | } 21 | } 22 | }) 23 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": ["node_modules", "build", "vite.config.ts"], 4 | "parser": "@typescript-eslint/parser", 5 | "parserOptions": { 6 | "project": "./tsconfig.json" 7 | }, 8 | "env": { 9 | "es2021": true, 10 | "browser": true, 11 | "node": true 12 | }, 13 | "extends": ["love", "prettier"], 14 | "rules": { 15 | "semi": ["off"], 16 | "@typescript-eslint/strict-boolean-expressions": ["off"], 17 | "@typescript-eslint/consistent-type-definitions": ["off"] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | on: 3 | push: 4 | branches: [main] 5 | pull_request: 6 | branches: [main] 7 | jobs: 8 | tests: 9 | timeout-minutes: 15 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | - uses: actions/setup-node@v3 14 | with: 15 | node-version: 20 16 | - run: npm ci 17 | - run: npm run clean 18 | - run: npm run build 19 | - run: npm run lint 20 | - run: npm run format-check 21 | - run: npm run test-unit -- --run 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | logs 3 | pids 4 | lib-cov 5 | coverage 6 | nbproject 7 | build 8 | build/Release 9 | typings/ 10 | *.tgz 11 | .nyc_output 12 | .pnp/ 13 | .pnp.js 14 | .next 15 | .env* 16 | .cache/ 17 | *.log 18 | *.pid 19 | *.pid.lock 20 | *.seed 21 | *~ 22 | *# 23 | .grunt 24 | .lock-wscript 25 | .tmp 26 | .sass-cache 27 | .netbeans 28 | .idea 29 | .npm 30 | .node_history 31 | .node_repl_history 32 | .vagrant 33 | .eslintcache 34 | .DS_STORE 35 | .DS_Store 36 | Thumbs.db 37 | dump.rdb 38 | npm-debug.log 39 | npm-debug.log* 40 | yarn-debug.log* 41 | yarn.lock 42 | yarn-error.log 43 | yarn-error.log* 44 | .yarn-integrity 45 | *.tsbuildinfo 46 | tsconfig.vitest-temp.json 47 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | logs 3 | pids 4 | lib-cov 5 | coverage 6 | nbproject 7 | build 8 | build/Release 9 | typings/ 10 | *.tgz 11 | .nyc_output 12 | .pnp/ 13 | .pnp.js 14 | .next 15 | .env* 16 | .cache/ 17 | *.log 18 | *.pid 19 | *.pid.lock 20 | *.seed 21 | *~ 22 | *# 23 | .grunt 24 | .lock-wscript 25 | .tmp 26 | .sass-cache 27 | .netbeans 28 | .idea 29 | .npm 30 | .node_history 31 | .node_repl_history 32 | .vagrant 33 | .eslintcache 34 | .DS_STORE 35 | .DS_Store 36 | Thumbs.db 37 | dump.rdb 38 | npm-debug.log 39 | npm-debug.log* 40 | yarn-debug.log* 41 | yarn.lock 42 | yarn-error.log 43 | yarn-error.log* 44 | .yarn-integrity 45 | *.tsbuildinfo 46 | tsconfig.vitest-temp.json 47 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode", 3 | "editor.formatOnSave": true, 4 | "[javascript]": { 5 | "editor.defaultFormatter": "esbenp.prettier-vscode" 6 | }, 7 | "[javascriptreact]": { 8 | "editor.defaultFormatter": "esbenp.prettier-vscode" 9 | }, 10 | "[typescript]": { 11 | "editor.defaultFormatter": "esbenp.prettier-vscode" 12 | }, 13 | "[typescriptreact]": { 14 | "editor.defaultFormatter": "esbenp.prettier-vscode" 15 | }, 16 | "[html]": { 17 | "editor.defaultFormatter": "esbenp.prettier-vscode" 18 | }, 19 | "[css]": { 20 | "editor.defaultFormatter": "esbenp.prettier-vscode" 21 | }, 22 | "[json]": { 23 | "editor.defaultFormatter": "esbenp.prettier-vscode" 24 | }, 25 | "[md]": { 26 | "editor.defaultFormatter": "esbenp.prettier-vscode" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 - 2024 Romel Perez (romelperez.dev) 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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ukti", 3 | "version": "4.1.1", 4 | "publishConfig": { 5 | "access": "public" 6 | }, 7 | "description": "~1kB Type-safe i18n and l10n JavaScript utility.", 8 | "keywords": [ 9 | "i18n", 10 | "l10n", 11 | "internationalization", 12 | "localization" 13 | ], 14 | "homepage": "https://github.com/romelperez/ukti", 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/romelperez/ukti.git" 18 | }, 19 | "funding": "https://github.com/sponsors/romelperez", 20 | "license": "MIT", 21 | "type": "module", 22 | "files": [ 23 | "build" 24 | ], 25 | "exports": { 26 | ".": { 27 | "import": "./build/esm/index.js", 28 | "require": "./build/cjs/index.js" 29 | } 30 | }, 31 | "types": "./build/esm/index.d.ts", 32 | "module": "./build/esm/index.js", 33 | "main": "./build/cjs/index.js", 34 | "unpkg": "./build/umd/ukti.umd.cjs", 35 | "devDependencies": { 36 | "eslint": "^8.57.0", 37 | "eslint-config-love": "^43.1.0", 38 | "eslint-config-prettier": "^9.1.0", 39 | "husky": "^9.0.11", 40 | "prettier": "^3.2.5", 41 | "typescript": "5.2", 42 | "vite": "^5.2.4", 43 | "vitest": "^1.4.0" 44 | }, 45 | "scripts": { 46 | "prepare": "husky", 47 | "clean": "rm -rf ./build && rm -f .tsbuildinfo", 48 | "build": "npm run build-esm && npm run build-cjs && npm run build-umd", 49 | "build-esm": "sh ./scripts/build-esm.sh", 50 | "build-cjs": "sh ./scripts/build-cjs.sh", 51 | "build-umd": "vite build", 52 | "dev": "sh ./scripts/build-esm.sh --watch", 53 | "format-check": "prettier . --check", 54 | "format": "prettier . --write", 55 | "lint": "eslint ./src/**/*.ts", 56 | "lint-fix": "eslint ./src/**/*.ts --fix", 57 | "test-unit": "vitest --typecheck", 58 | "test": "npm run clean && npm run build && npm run format-check && npm run lint && npm run test-unit -- --run", 59 | "prepublishOnly": "npm run test" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/createUktiTranslator.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | UktiLanguages, 3 | UktiRegions, 4 | UktiDefinition, 5 | UktiTranslations, 6 | UktiTranslator, 7 | UktiTranslate 8 | } from './types.js' 9 | import { UKTI_LANGUAGE_DEFAULT } from './constants.js' 10 | import { renderUktiTemplate } from './renderUktiTemplate.js' 11 | 12 | const createUktiTranslator = < 13 | Definition extends UktiDefinition, 14 | Languages extends string = UktiLanguages, 15 | LanguageDefault extends string = typeof UKTI_LANGUAGE_DEFAULT, 16 | Regions extends string = UktiRegions 17 | >( 18 | props: { 19 | translations: UktiTranslations 20 | throwIfError?: boolean 21 | } & (LanguageDefault extends typeof UKTI_LANGUAGE_DEFAULT 22 | ? { 23 | languageDefault?: LanguageDefault 24 | } 25 | : { 26 | languageDefault: LanguageDefault 27 | }) 28 | ): UktiTranslator => { 29 | const { translations, throwIfError, languageDefault } = props 30 | 31 | const definitionDefault = 32 | translations[(languageDefault ?? UKTI_LANGUAGE_DEFAULT) as LanguageDefault] 33 | 34 | if (definitionDefault === null || typeof definitionDefault !== 'object') { 35 | throw new Error('Ukti requires the translations to have at least the default language.') 36 | } 37 | 38 | const getTranslation = (template: string, variables?: Record): string => 39 | renderUktiTemplate(template, variables, { throwIfError }) 40 | 41 | return (language: Languages, region?: Regions): UktiTranslate => { 42 | const definition = translations[language] ?? definitionDefault 43 | 44 | const proxy = new Proxy( 45 | {}, 46 | { 47 | get(target, property1: string) { 48 | if (property1 === 'regions') { 49 | console.error('Ukti translations have the word "regions" reserved.') 50 | return () => '' 51 | } 52 | 53 | const structure1 = definitionDefault[property1 as keyof Definition] 54 | const level1 = definition[property1 as keyof Definition] 55 | 56 | if (structure1 !== null && typeof structure1 === 'object') { 57 | return new Proxy( 58 | {}, 59 | { 60 | get(target, property2: string) { 61 | const level2 = (level1 as Record)?.[property2] 62 | 63 | return (variables?: Record): string => { 64 | if (!level2) { 65 | return '' 66 | } 67 | 68 | const regionText = region 69 | ? (definition.regions?.[region]?.[property1] as Record)?.[ 70 | property2 71 | ] 72 | : false 73 | 74 | if (regionText) { 75 | return getTranslation(regionText, variables) 76 | } 77 | 78 | return getTranslation(level2 as string, variables) 79 | } 80 | } 81 | } 82 | ) 83 | } 84 | 85 | return (variables?: Record): string => { 86 | if (!level1) { 87 | return '' 88 | } 89 | 90 | const regionText = region 91 | ? (definition.regions?.[region]?.[property1] as string) 92 | : false 93 | 94 | if (regionText) { 95 | return getTranslation(regionText, variables) 96 | } 97 | 98 | return getTranslation(level1 as string, variables) 99 | } 100 | } 101 | } 102 | ) as UktiTranslate 103 | 104 | return proxy 105 | } 106 | } 107 | 108 | export { createUktiTranslator } 109 | -------------------------------------------------------------------------------- /src/renderUktiTemplate.ts: -------------------------------------------------------------------------------- 1 | const showVariableError = ( 2 | variableName: string, 3 | config: { throwIfError?: boolean } | undefined 4 | ): void => { 5 | const error = `Ukti template requires defined variable "${variableName}" to render.` 6 | if (config?.throwIfError) { 7 | throw new Error(error) 8 | } else { 9 | console.error(error) 10 | } 11 | } 12 | 13 | const getInterpolation = ( 14 | data: string, 15 | variables: Record, 16 | config: { throwIfError?: boolean } | undefined 17 | ): number | string | undefined => { 18 | const value = data.trim() 19 | 20 | // Is variable. 21 | if (/^[a-zA-Z]\w*$/.test(value)) { 22 | if (variables[value] === undefined) { 23 | showVariableError(value, config) 24 | } 25 | return variables[value] as string 26 | } 27 | // Is numeric. 28 | else if (/^-?\d+(.\d+)?$/.test(value)) { 29 | return Number(value) 30 | } 31 | 32 | // Is string. 33 | return value.trim().replace(/^["']/, '').replace(/["']$/, '') 34 | } 35 | 36 | /** 37 | * Render a template string with optional variables if required. 38 | * All interpolations have to be wrapped in "{{" and "}}". 39 | * It suppots the following template interpolations: 40 | * - "{{value}}" for basic interpolation. 41 | * - "{{value ? value : value}}" for interpolation with comparison. 42 | * - "{{value comparator value ? value : value}}" for interpolation with comparison 43 | * with comparator. 44 | * @param text Template text. 45 | * @param variables Optional variables. 46 | * @returns Interpolated template string with variables. 47 | */ 48 | const renderUktiTemplate = = Record>( 49 | template: string, 50 | variables?: Vars, 51 | config?: { throwIfError?: boolean } 52 | ): string => { 53 | if (!variables || typeof variables !== 'object') { 54 | return template 55 | } 56 | 57 | const matches = template.match(/{{([\s\S]+?)}}/g) 58 | 59 | if (!matches?.length) { 60 | return template 61 | } 62 | 63 | return [...matches].reduce((text, item) => { 64 | let result = item.replace(/^{{/, '').replace(/}}$/, '').trim() 65 | 66 | // Is basic interpolation. 67 | if (Object.keys(variables).includes(result)) { 68 | if (variables[result] === undefined) { 69 | showVariableError(result, config) 70 | return '' 71 | } 72 | result = variables[result] as string 73 | } 74 | // Is a conditional. 75 | else if (/^.+\?.+:.+$/.test(result)) { 76 | const [condition, truthy, falsy] = result.split(/[?:]/) 77 | let isValid = false 78 | 79 | // Conditional has comparator in format " statement comparator statement ". 80 | if (/^[\s\S]+(===?|!==?|>=?|<=?)[\s\S]+$/.test(condition)) { 81 | const fragments = condition.split(/(===?|!==?|>=?|<=?)/) 82 | 83 | const x = getInterpolation(fragments[0], variables, config) 84 | const comparator = fragments[1] 85 | const y = getInterpolation(fragments[2], variables, config) 86 | 87 | if (x === undefined || y === undefined) { 88 | return '' 89 | } 90 | 91 | switch (comparator) { 92 | case '==': 93 | case '===': 94 | isValid = x === y 95 | break 96 | case '!=': 97 | case '!==': 98 | isValid = x !== y 99 | break 100 | case '>': 101 | isValid = x > y 102 | break 103 | case '>=': 104 | isValid = x >= y 105 | break 106 | case '<': 107 | isValid = x < y 108 | break 109 | case '<=': 110 | isValid = x <= y 111 | break 112 | } 113 | } 114 | // Conditional has truthy variable. 115 | else { 116 | const variable = getInterpolation(condition, variables, config) 117 | if (variable === undefined) { 118 | return '' 119 | } 120 | isValid = !!variable 121 | } 122 | 123 | const value = getInterpolation(isValid ? truthy : falsy, variables, config) 124 | 125 | if (value === undefined) { 126 | return '' 127 | } 128 | 129 | result = value as string 130 | } 131 | // Unknown interpolation. 132 | else { 133 | showVariableError(result, config) 134 | return '' 135 | } 136 | 137 | return text.replace(item, result) 138 | }, template) 139 | } 140 | 141 | export { renderUktiTemplate } 142 | -------------------------------------------------------------------------------- /tests/translator.test-d.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'vitest' 2 | import { createUktiTranslator } from '../' 3 | 4 | test('Should type-safe first level translation definitions', () => { 5 | type Definition = { 6 | x: undefined 7 | y: [{ a: number; b: string }] 8 | } 9 | const translator = createUktiTranslator({ 10 | translations: { 11 | en: { 12 | x: 'x', 13 | y: 'y' 14 | } 15 | } 16 | }) 17 | const t = translator('en') 18 | // @ts-expect-error test 19 | t.x({}) 20 | // @ts-expect-error test 21 | t.x({ a: 1, b: '2' }) 22 | // @ts-expect-error test 23 | t.y() 24 | // @ts-expect-error test 25 | t.y({ b: 2 }) 26 | // @ts-expect-error test 27 | t.s() 28 | // @ts-expect-error test 29 | t.s({}) 30 | }) 31 | 32 | test('Should type-safe second level translation definitions', () => { 33 | type Definition = { 34 | w: undefined 35 | p: { 36 | x: undefined 37 | y: [{ a: number; b: string }] 38 | } 39 | } 40 | const translator = createUktiTranslator({ 41 | translations: { 42 | en: { 43 | w: 'w', 44 | p: { 45 | x: 'x', 46 | y: 'y' 47 | } 48 | } 49 | } 50 | }) 51 | const t = translator('en') 52 | // @ts-expect-error test 53 | t.p.x({}) 54 | // @ts-expect-error test 55 | t.p.x({ a: 1, b: '2' }) 56 | // @ts-expect-error test 57 | t.p.y() 58 | // @ts-expect-error test 59 | t.p.y({ b: 2 }) 60 | // @ts-expect-error test 61 | t.s() 62 | // @ts-expect-error test 63 | t.s({}) 64 | // @ts-expect-error test 65 | t.p.s() 66 | // @ts-expect-error test 67 | t.p.s({}) 68 | }) 69 | 70 | test('Should accept custom languages and default language', () => { 71 | type Definition = { 72 | a: undefined 73 | } 74 | type Languages = 'fr' | 'hi' | 'zh' 75 | type LanguageDefault = 'hi' 76 | const translator1 = createUktiTranslator({ 77 | languageDefault: 'hi', 78 | translations: { hi: { a: 'a' } } 79 | }) 80 | // @ts-expect-error test 81 | translator1('es') 82 | createUktiTranslator({ 83 | // @ts-expect-error test 84 | languageDefault: 'xx', 85 | translations: { hi: { a: 'a' } } 86 | }) 87 | 88 | createUktiTranslator({ 89 | languageDefault: 'hi', 90 | translations: { 91 | // @ts-expect-error test 92 | es: { 93 | a: 'a' 94 | } 95 | } 96 | }) 97 | }) 98 | 99 | test('Should type-safe regions', () => { 100 | type Definition = { 101 | a: undefined 102 | } 103 | type Languages = 'fr' | 'hi' | 'zh' 104 | type LanguageDefault = 'hi' 105 | createUktiTranslator({ 106 | languageDefault: 'hi', 107 | translations: { 108 | hi: { 109 | a: 'a', 110 | regions: { 111 | CO: { 112 | a: 'b' 113 | }, 114 | // @ts-expect-error test 115 | X: { 116 | a: 'y' 117 | } 118 | } 119 | } 120 | } 121 | }) 122 | createUktiTranslator({ 123 | languageDefault: 'hi', 124 | translations: { 125 | hi: { 126 | a: 'a', 127 | regions: { 128 | CO: { 129 | // @ts-expect-error test 130 | b: 'b' 131 | } 132 | } 133 | } 134 | } 135 | }) 136 | }) 137 | 138 | test('Should type-safe custom languages', () => { 139 | type Definition = { 140 | a: undefined 141 | } 142 | type Languages = 'x' | 'y' 143 | type LanguageDefault = 'x' 144 | createUktiTranslator({ 145 | languageDefault: 'x', 146 | translations: { 147 | x: { 148 | a: 'a' 149 | } 150 | } 151 | }) 152 | createUktiTranslator({ 153 | // @ts-expect-error test 154 | languageDefault: 'z', 155 | translations: { 156 | x: { 157 | a: 'a' 158 | } 159 | } 160 | }) 161 | createUktiTranslator({ 162 | languageDefault: 'x', 163 | // @ts-expect-error test 164 | translations: { 165 | y: { 166 | a: 'a' 167 | } 168 | } 169 | }) 170 | createUktiTranslator({ 171 | languageDefault: 'x', 172 | translations: { 173 | // @ts-expect-error test 174 | z: { 175 | a: 'a' 176 | } 177 | } 178 | }) 179 | }) 180 | 181 | test('Should type-safe custom regions', () => { 182 | type Definition = { 183 | a: undefined 184 | } 185 | type Languages = 'fr' | 'hi' | 'zh' 186 | type LanguageDefault = 'hi' 187 | type Regions = 'X' | 'Y' 188 | createUktiTranslator({ 189 | languageDefault: 'hi', 190 | translations: { 191 | hi: { 192 | a: 'a', 193 | regions: { 194 | X: { 195 | a: 'b' 196 | }, 197 | // @ts-expect-error test 198 | Z: { 199 | a: 'c' 200 | } 201 | } 202 | } 203 | } 204 | }) 205 | createUktiTranslator({ 206 | languageDefault: 'hi', 207 | translations: { 208 | hi: { 209 | a: 'a', 210 | regions: { 211 | Y: { 212 | // @ts-expect-error test 213 | b: 'b' 214 | } 215 | } 216 | } 217 | } 218 | }) 219 | }) 220 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ## GITATTRIBUTES FOR WEB PROJECTS 2 | # 3 | # These settings are for any web project. 4 | # 5 | # Details per file setting: 6 | # text These files should be normalized (i.e. convert CRLF to LF). 7 | # binary These files are binary and should be left untouched. 8 | # 9 | # Note that binary is a macro for -text -diff. 10 | ###################################################################### 11 | 12 | # Auto detect 13 | ## Handle line endings automatically for files detected as 14 | ## text and leave all files detected as binary untouched. 15 | ## This will handle all files NOT defined below. 16 | * text=auto 17 | 18 | # Source code 19 | *.bash text eol=lf 20 | *.bat text eol=crlf 21 | *.cmd text eol=crlf 22 | *.coffee text 23 | *.css text diff=css 24 | *.htm text diff=html 25 | *.html text diff=html 26 | *.inc text 27 | *.ini text 28 | *.js text 29 | *.json text 30 | *.jsx text 31 | *.less text 32 | *.ls text 33 | *.map text -diff 34 | *.od text 35 | *.onlydata text 36 | *.php text diff=php 37 | *.pl text 38 | *.ps1 text eol=crlf 39 | *.py text diff=python 40 | *.rb text diff=ruby 41 | *.sass text 42 | *.scm text 43 | *.scss text diff=css 44 | *.sh text eol=lf 45 | *.sql text 46 | *.styl text 47 | *.tag text 48 | *.ts text 49 | *.tsx text 50 | *.xml text 51 | *.xhtml text diff=html 52 | 53 | # Docker 54 | Dockerfile text 55 | 56 | # Documentation 57 | *.ipynb text 58 | *.markdown text diff=markdown 59 | *.md text diff=markdown 60 | *.mdwn text diff=markdown 61 | *.mdown text diff=markdown 62 | *.mkd text diff=markdown 63 | *.mkdn text diff=markdown 64 | *.mdtxt text 65 | *.mdtext text 66 | *.txt text 67 | AUTHORS text 68 | CHANGELOG text 69 | CHANGES text 70 | CONTRIBUTING text 71 | COPYING text 72 | copyright text 73 | *COPYRIGHT* text 74 | INSTALL text 75 | license text 76 | LICENSE text 77 | NEWS text 78 | readme text 79 | *README* text 80 | TODO text 81 | 82 | # Templates 83 | *.dot text 84 | *.ejs text 85 | *.haml text 86 | *.handlebars text 87 | *.hbs text 88 | *.hbt text 89 | *.jade text 90 | *.latte text 91 | *.mustache text 92 | *.njk text 93 | *.phtml text 94 | *.svelte text 95 | *.tmpl text 96 | *.tpl text 97 | *.twig text 98 | *.vue text 99 | 100 | # Configs 101 | *.cnf text 102 | *.conf text 103 | *.config text 104 | .editorconfig text 105 | .env text 106 | .gitattributes text 107 | .gitconfig text 108 | .htaccess text 109 | *.lock text -diff 110 | package.json text eol=lf 111 | package-lock.json text -diff 112 | pnpm-lock.yaml text eol=lf -diff 113 | .prettierrc text 114 | yarn.lock text -diff 115 | *.toml text 116 | *.yaml text 117 | *.yml text 118 | browserslist text 119 | Makefile text 120 | makefile text 121 | 122 | # Heroku 123 | Procfile text 124 | 125 | # Graphics 126 | *.ai binary 127 | *.bmp binary 128 | *.eps binary 129 | *.gif binary 130 | *.gifv binary 131 | *.ico binary 132 | *.jng binary 133 | *.jp2 binary 134 | *.jpg binary 135 | *.jpeg binary 136 | *.jpx binary 137 | *.jxr binary 138 | *.pdf binary 139 | *.png binary 140 | *.psb binary 141 | *.psd binary 142 | # SVG treated as an asset (binary) by default. 143 | *.svg text 144 | # If you want to treat it as binary, 145 | # use the following line instead. 146 | # *.svg binary 147 | *.svgz binary 148 | *.tif binary 149 | *.tiff binary 150 | *.wbmp binary 151 | *.webp binary 152 | 153 | # Audio 154 | *.kar binary 155 | *.m4a binary 156 | *.mid binary 157 | *.midi binary 158 | *.mp3 binary 159 | *.ogg binary 160 | *.ra binary 161 | 162 | # Video 163 | *.3gpp binary 164 | *.3gp binary 165 | *.as binary 166 | *.asf binary 167 | *.asx binary 168 | *.avi binary 169 | *.fla binary 170 | *.flv binary 171 | *.m4v binary 172 | *.mng binary 173 | *.mov binary 174 | *.mp4 binary 175 | *.mpeg binary 176 | *.mpg binary 177 | *.ogv binary 178 | *.swc binary 179 | *.swf binary 180 | *.webm binary 181 | 182 | # Archives 183 | *.7z binary 184 | *.gz binary 185 | *.jar binary 186 | *.rar binary 187 | *.tar binary 188 | *.zip binary 189 | 190 | # Fonts 191 | *.ttf binary 192 | *.eot binary 193 | *.otf binary 194 | *.woff binary 195 | *.woff2 binary 196 | 197 | # Executables 198 | *.exe binary 199 | *.pyc binary 200 | 201 | # RC files (like .babelrc or .eslintrc) 202 | *.*rc text 203 | 204 | # Ignore files (like .npmignore or .gitignore) 205 | *.*ignore text 206 | -------------------------------------------------------------------------------- /tests/templates.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect, vi, afterEach } from 'vitest' 2 | import { renderUktiTemplate } from '../' 3 | 4 | afterEach(() => { 5 | vi.restoreAllMocks() 6 | }) 7 | 8 | test('Should render template variable', () => { 9 | const template = '{{v}} a {{ v}} b {{v }} c {{ v }}' 10 | expect(renderUktiTemplate(template, { v: 7 })).toBe('7 a 7 b 7 c 7') 11 | }) 12 | 13 | test('Should render template truthy variable in conditional with string/numeric values with double quotes', () => { 14 | const template = 'a {{v ? "p" : "q"}} b {{v ? 10 : 20}} c' 15 | expect(renderUktiTemplate(template, { v: true })).toBe('a p b 10 c') 16 | expect(renderUktiTemplate(template, { v: false })).toBe('a q b 20 c') 17 | }) 18 | 19 | test('Should render template truthy variable in conditional with string/numeric values with single quotes', () => { 20 | const template = "a {{v ? 'p' : 'q'}} b {{v ? 10 : 20}} c" 21 | expect(renderUktiTemplate(template, { v: true })).toBe('a p b 10 c') 22 | expect(renderUktiTemplate(template, { v: false })).toBe('a q b 20 c') 23 | }) 24 | 25 | test('Should render template conditional values for scapes quotes', () => { 26 | const template = 'a {{v ? \'\'p\'\' : ""q""}} b {{v ? 10 : 20}} c' 27 | expect(renderUktiTemplate(template, { v: true })).toBe("a 'p' b 10 c") 28 | expect(renderUktiTemplate(template, { v: false })).toBe('a "q" b 20 c') 29 | }) 30 | 31 | test('Should render template conditional with empty strings', () => { 32 | const template = 'There {{isUnit ? "is" : "are"}} {{qty}} product{{isUnit ? "" : "s"}} available' 33 | expect(renderUktiTemplate(template, { qty: 1, isUnit: true })).toBe( 34 | 'There is 1 product available' 35 | ) 36 | expect(renderUktiTemplate(template, { qty: 3, isUnit: false })).toBe( 37 | 'There are 3 products available' 38 | ) 39 | }) 40 | 41 | test('Should render template conditional with "==" comparator', () => { 42 | const template = 'product{{w == 1 ? "" : "s"}}' 43 | expect(renderUktiTemplate(template, { w: 1 })).toBe('product') 44 | expect(renderUktiTemplate(template, { w: 3 })).toBe('products') 45 | }) 46 | 47 | test('Should render template conditional with "===" comparator', () => { 48 | const template = 'product{{w === 1 ? "" : "s"}}' 49 | expect(renderUktiTemplate(template, { w: 1 })).toBe('product') 50 | expect(renderUktiTemplate(template, { w: 3 })).toBe('products') 51 | }) 52 | 53 | test('Should render template conditional with "!=" comparator', () => { 54 | const template = 'product{{w != 1 ? "s" : ""}}' 55 | expect(renderUktiTemplate(template, { w: 1 })).toBe('product') 56 | expect(renderUktiTemplate(template, { w: 3 })).toBe('products') 57 | }) 58 | 59 | test('Should render template conditional with "!==" comparator', () => { 60 | const template = 'product{{w !== 1 ? "s" : ""}}' 61 | expect(renderUktiTemplate(template, { w: 1 })).toBe('product') 62 | expect(renderUktiTemplate(template, { w: 3 })).toBe('products') 63 | }) 64 | 65 | test('Should render template conditional with ">" comparator', () => { 66 | const template = '{{w > 1 ? "a" : "b"}}' 67 | expect(renderUktiTemplate(template, { w: 1 })).toBe('b') 68 | expect(renderUktiTemplate(template, { w: 2 })).toBe('a') 69 | }) 70 | 71 | test('Should render template conditional with ">=" comparator', () => { 72 | const template = '{{w >= 1 ? "a" : "b"}}' 73 | expect(renderUktiTemplate(template, { w: 0 })).toBe('b') 74 | expect(renderUktiTemplate(template, { w: 1 })).toBe('a') 75 | expect(renderUktiTemplate(template, { w: 2 })).toBe('a') 76 | }) 77 | 78 | test('Should render template conditional with "<" comparator', () => { 79 | const template = '{{w < 1 ? "a" : "b"}}' 80 | expect(renderUktiTemplate(template, { w: 0 })).toBe('a') 81 | expect(renderUktiTemplate(template, { w: 1 })).toBe('b') 82 | expect(renderUktiTemplate(template, { w: 2 })).toBe('b') 83 | }) 84 | 85 | test('Should render template conditional with "<=" comparator', () => { 86 | const template = '{{w <= 1 ? "a" : "b"}}' 87 | expect(renderUktiTemplate(template, { w: 0 })).toBe('a') 88 | expect(renderUktiTemplate(template, { w: 1 })).toBe('a') 89 | expect(renderUktiTemplate(template, { w: 2 })).toBe('b') 90 | }) 91 | 92 | test('Should render template with multiple variables', () => { 93 | const template = 94 | 'The land vehicle{{length == 1 ? "" : "s"}} used {{length == 1 ? "is" : "are"}} {{items}} in the {{location}}.' 95 | const items = new // @ts-expect-error browser api 96 | Intl.ListFormat('en', { style: 'long', type: 'conjunction' }).format(['Motorcycle', 'Bus', 'Car']) 97 | 98 | expect( 99 | renderUktiTemplate(template, { items, length: items.length, location: 'countryside' }) 100 | ).toBe('The land vehicles used are Motorcycle, Bus, and Car in the countryside.') 101 | }) 102 | 103 | test('Should console error if required variable is not defined', () => { 104 | const consoleError = vi.spyOn(console, 'error') 105 | const template = '{{a}}' 106 | expect(renderUktiTemplate(template, {} as any)).toBe('') 107 | expect(consoleError).toHaveBeenCalledTimes(1) 108 | expect(consoleError).toHaveBeenCalledWith( 109 | 'Ukti template requires defined variable "a" to render.' 110 | ) 111 | expect(renderUktiTemplate(template, { a: undefined } as any)).toBe('') 112 | expect(consoleError).toHaveBeenCalledTimes(2) 113 | expect(consoleError).toHaveBeenCalledWith( 114 | 'Ukti template requires defined variable "a" to render.' 115 | ) 116 | }) 117 | 118 | test('Should console error if required variable in conditional is not defined', () => { 119 | const consoleError = vi.spyOn(console, 'error') 120 | const template = '{{a ? b : c}}' 121 | expect(renderUktiTemplate(template, { b: 2, c: 3 } as any)).toBe('') 122 | expect(consoleError).toHaveBeenCalledTimes(1) 123 | expect(consoleError).toHaveBeenCalledWith( 124 | 'Ukti template requires defined variable "a" to render.' 125 | ) 126 | expect(renderUktiTemplate(template, { a: true, c: 3 } as any)).toBe('') 127 | expect(consoleError).toHaveBeenCalledTimes(2) 128 | expect(consoleError).toHaveBeenCalledWith( 129 | 'Ukti template requires defined variable "b" to render.' 130 | ) 131 | expect(renderUktiTemplate(template, { a: false, b: 3 } as any)).toBe('') 132 | expect(consoleError).toHaveBeenCalledTimes(3) 133 | expect(consoleError).toHaveBeenCalledWith( 134 | 'Ukti template requires defined variable "c" to render.' 135 | ) 136 | expect(renderUktiTemplate(template, { a: true, b: 3, c: 4 } as any)).toBe('3') 137 | expect(consoleError).toHaveBeenCalledTimes(3) 138 | }) 139 | 140 | test('Should console error if required variable in conditional with comparator is not defined', () => { 141 | const consoleError = vi.spyOn(console, 'error') 142 | const template = '{{a > b ? c : d}}' 143 | expect(renderUktiTemplate(template, { b: 0, c: 3, d: 4 } as any)).toBe('') 144 | expect(consoleError).toHaveBeenCalledTimes(1) 145 | expect(consoleError).toHaveBeenCalledWith( 146 | 'Ukti template requires defined variable "a" to render.' 147 | ) 148 | expect(renderUktiTemplate(template, { a: 0, c: 3, d: 4 } as any)).toBe('') 149 | expect(consoleError).toHaveBeenCalledTimes(2) 150 | expect(consoleError).toHaveBeenCalledWith( 151 | 'Ukti template requires defined variable "b" to render.' 152 | ) 153 | expect(renderUktiTemplate(template, { a: 1, b: 0, d: 4 } as any)).toBe('') 154 | expect(consoleError).toHaveBeenCalledTimes(3) 155 | expect(consoleError).toHaveBeenCalledWith( 156 | 'Ukti template requires defined variable "c" to render.' 157 | ) 158 | expect(renderUktiTemplate(template, { a: 1, b: 2, c: 3 } as any)).toBe('') 159 | expect(consoleError).toHaveBeenCalledTimes(4) 160 | expect(consoleError).toHaveBeenCalledWith( 161 | 'Ukti template requires defined variable "d" to render.' 162 | ) 163 | expect(renderUktiTemplate(template, { a: 1, b: 2, c: 3, d: 4 } as any)).toBe('4') 164 | expect(consoleError).toHaveBeenCalledTimes(4) 165 | }) 166 | 167 | test('Should throw error if required variable is not defined and "throwIfError" enabled', () => { 168 | const template = '{{qty}} products' 169 | expect(() => renderUktiTemplate(template, {} as any, { throwIfError: true })).toThrowError( 170 | 'Ukti template requires defined variable "qty" to render.' 171 | ) 172 | }) 173 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | // ISO 639-1 2 | export type UktiLanguages = 3 | | 'ab' 4 | | 'aa' 5 | | 'af' 6 | | 'ak' 7 | | 'sq' 8 | | 'am' 9 | | 'ar' 10 | | 'an' 11 | | 'hy' 12 | | 'as' 13 | | 'av' 14 | | 'ae' 15 | | 'ay' 16 | | 'az' 17 | | 'bm' 18 | | 'ba' 19 | | 'eu' 20 | | 'be' 21 | | 'bn' 22 | | 'bi' 23 | | 'bs' 24 | | 'br' 25 | | 'bg' 26 | | 'my' 27 | | 'ca' 28 | | 'ch' 29 | | 'ce' 30 | | 'ny' 31 | | 'zh' 32 | | 'cu' 33 | | 'cv' 34 | | 'kw' 35 | | 'co' 36 | | 'cr' 37 | | 'hr' 38 | | 'cs' 39 | | 'da' 40 | | 'dv' 41 | | 'nl' 42 | | 'dz' 43 | | 'en' 44 | | 'eo' 45 | | 'et' 46 | | 'ee' 47 | | 'fo' 48 | | 'fj' 49 | | 'fi' 50 | | 'fr' 51 | | 'fy' 52 | | 'ff' 53 | | 'gd' 54 | | 'gl' 55 | | 'lg' 56 | | 'ka' 57 | | 'de' 58 | | 'el' 59 | | 'kl' 60 | | 'gn' 61 | | 'gu' 62 | | 'ht' 63 | | 'ha' 64 | | 'he' 65 | | 'hz' 66 | | 'hi' 67 | | 'ho' 68 | | 'hu' 69 | | 'is' 70 | | 'io' 71 | | 'ig' 72 | | 'id' 73 | | 'ia' 74 | | 'ie' 75 | | 'iu' 76 | | 'ik' 77 | | 'ga' 78 | | 'it' 79 | | 'ja' 80 | | 'jv' 81 | | 'kn' 82 | | 'kr' 83 | | 'ks' 84 | | 'kk' 85 | | 'km' 86 | | 'ki' 87 | | 'rw' 88 | | 'ky' 89 | | 'kv' 90 | | 'kg' 91 | | 'ko' 92 | | 'kj' 93 | | 'ku' 94 | | 'lo' 95 | | 'la' 96 | | 'lv' 97 | | 'li' 98 | | 'ln' 99 | | 'lt' 100 | | 'lu' 101 | | 'lb' 102 | | 'mk' 103 | | 'mg' 104 | | 'ms' 105 | | 'ml' 106 | | 'mt' 107 | | 'gv' 108 | | 'mi' 109 | | 'mr' 110 | | 'mh' 111 | | 'mn' 112 | | 'na' 113 | | 'nv' 114 | | 'nd' 115 | | 'nr' 116 | | 'ng' 117 | | 'ne' 118 | | 'no' 119 | | 'nb' 120 | | 'nn' 121 | | 'ii' 122 | | 'oc' 123 | | 'oj' 124 | | 'or' 125 | | 'om' 126 | | 'os' 127 | | 'pi' 128 | | 'ps' 129 | | 'fa' 130 | | 'pl' 131 | | 'pt' 132 | | 'pa' 133 | | 'qu' 134 | | 'ro' 135 | | 'rm' 136 | | 'rn' 137 | | 'ru' 138 | | 'se' 139 | | 'sm' 140 | | 'sg' 141 | | 'sa' 142 | | 'sc' 143 | | 'sr' 144 | | 'sn' 145 | | 'sd' 146 | | 'si' 147 | | 'sk' 148 | | 'sl' 149 | | 'so' 150 | | 'st' 151 | | 'es' 152 | | 'su' 153 | | 'sw' 154 | | 'ss' 155 | | 'sv' 156 | | 'tl' 157 | | 'ty' 158 | | 'tg' 159 | | 'ta' 160 | | 'tt' 161 | | 'te' 162 | | 'th' 163 | | 'bo' 164 | | 'ti' 165 | | 'to' 166 | | 'ts' 167 | | 'tn' 168 | | 'tr' 169 | | 'tk' 170 | | 'tw' 171 | | 'ug' 172 | | 'uk' 173 | | 'ur' 174 | | 'uz' 175 | | 've' 176 | | 'vi' 177 | | 'vo' 178 | | 'wa' 179 | | 'cy' 180 | | 'wo' 181 | | 'xh' 182 | | 'yi' 183 | | 'yo' 184 | | 'za' 185 | | 'zu' 186 | 187 | // ISO 3166-1 alpha-2 188 | export type UktiRegions = 189 | | 'AD' 190 | | 'AE' 191 | | 'AF' 192 | | 'AG' 193 | | 'AI' 194 | | 'AL' 195 | | 'AM' 196 | | 'AO' 197 | | 'AQ' 198 | | 'AR' 199 | | 'AS' 200 | | 'AT' 201 | | 'AU' 202 | | 'AW' 203 | | 'AX' 204 | | 'AZ' 205 | | 'BA' 206 | | 'BB' 207 | | 'BD' 208 | | 'BE' 209 | | 'BF' 210 | | 'BG' 211 | | 'BH' 212 | | 'BI' 213 | | 'BJ' 214 | | 'BL' 215 | | 'BM' 216 | | 'BN' 217 | | 'BO' 218 | | 'BQ' 219 | | 'BR' 220 | | 'BS' 221 | | 'BT' 222 | | 'BV' 223 | | 'BW' 224 | | 'BY' 225 | | 'BZ' 226 | | 'CA' 227 | | 'CC' 228 | | 'CD' 229 | | 'CF' 230 | | 'CG' 231 | | 'CH' 232 | | 'CI' 233 | | 'CK' 234 | | 'CL' 235 | | 'CM' 236 | | 'CN' 237 | | 'CO' 238 | | 'CR' 239 | | 'CU' 240 | | 'CV' 241 | | 'CW' 242 | | 'CX' 243 | | 'CY' 244 | | 'CZ' 245 | | 'DE' 246 | | 'DJ' 247 | | 'DK' 248 | | 'DM' 249 | | 'DO' 250 | | 'DZ' 251 | | 'EC' 252 | | 'EE' 253 | | 'EG' 254 | | 'EH' 255 | | 'ER' 256 | | 'ES' 257 | | 'ET' 258 | | 'FI' 259 | | 'FJ' 260 | | 'FK' 261 | | 'FM' 262 | | 'FO' 263 | | 'FR' 264 | | 'GA' 265 | | 'GB' 266 | | 'GD' 267 | | 'GE' 268 | | 'GF' 269 | | 'GG' 270 | | 'GH' 271 | | 'GI' 272 | | 'GL' 273 | | 'GM' 274 | | 'GN' 275 | | 'GP' 276 | | 'GQ' 277 | | 'GR' 278 | | 'GS' 279 | | 'GT' 280 | | 'GU' 281 | | 'GW' 282 | | 'GY' 283 | | 'HK' 284 | | 'HM' 285 | | 'HN' 286 | | 'HR' 287 | | 'HT' 288 | | 'HU' 289 | | 'ID' 290 | | 'IE' 291 | | 'IL' 292 | | 'IM' 293 | | 'IN' 294 | | 'IO' 295 | | 'IQ' 296 | | 'IR' 297 | | 'IS' 298 | | 'IT' 299 | | 'JE' 300 | | 'JM' 301 | | 'JO' 302 | | 'JP' 303 | | 'KE' 304 | | 'KG' 305 | | 'KH' 306 | | 'KI' 307 | | 'KM' 308 | | 'KN' 309 | | 'KP' 310 | | 'KR' 311 | | 'KW' 312 | | 'KY' 313 | | 'KZ' 314 | | 'LA' 315 | | 'LB' 316 | | 'LC' 317 | | 'LI' 318 | | 'LK' 319 | | 'LR' 320 | | 'LS' 321 | | 'LT' 322 | | 'LU' 323 | | 'LV' 324 | | 'LY' 325 | | 'MA' 326 | | 'MC' 327 | | 'MD' 328 | | 'ME' 329 | | 'MF' 330 | | 'MG' 331 | | 'MH' 332 | | 'MK' 333 | | 'ML' 334 | | 'MM' 335 | | 'MN' 336 | | 'MO' 337 | | 'MP' 338 | | 'MQ' 339 | | 'MR' 340 | | 'MS' 341 | | 'MT' 342 | | 'MU' 343 | | 'MV' 344 | | 'MW' 345 | | 'MX' 346 | | 'MY' 347 | | 'MZ' 348 | | 'NA' 349 | | 'NC' 350 | | 'NE' 351 | | 'NF' 352 | | 'NG' 353 | | 'NI' 354 | | 'NL' 355 | | 'NO' 356 | | 'NP' 357 | | 'NR' 358 | | 'NU' 359 | | 'NZ' 360 | | 'OM' 361 | | 'PA' 362 | | 'PE' 363 | | 'PF' 364 | | 'PG' 365 | | 'PH' 366 | | 'PK' 367 | | 'PL' 368 | | 'PM' 369 | | 'PN' 370 | | 'PR' 371 | | 'PS' 372 | | 'PT' 373 | | 'PW' 374 | | 'PY' 375 | | 'QA' 376 | | 'RE' 377 | | 'RO' 378 | | 'RS' 379 | | 'RU' 380 | | 'RW' 381 | | 'SA' 382 | | 'SB' 383 | | 'SC' 384 | | 'SD' 385 | | 'SE' 386 | | 'SG' 387 | | 'SH' 388 | | 'SI' 389 | | 'SJ' 390 | | 'SK' 391 | | 'SL' 392 | | 'SM' 393 | | 'SN' 394 | | 'SO' 395 | | 'SR' 396 | | 'SS' 397 | | 'ST' 398 | | 'SV' 399 | | 'SX' 400 | | 'SY' 401 | | 'SZ' 402 | | 'TC' 403 | | 'TD' 404 | | 'TF' 405 | | 'TG' 406 | | 'TH' 407 | | 'TJ' 408 | | 'TK' 409 | | 'TL' 410 | | 'TM' 411 | | 'TN' 412 | | 'TO' 413 | | 'TR' 414 | | 'TT' 415 | | 'TV' 416 | | 'TW' 417 | | 'TZ' 418 | | 'UA' 419 | | 'UG' 420 | | 'UM' 421 | | 'US' 422 | | 'UY' 423 | | 'UZ' 424 | | 'VA' 425 | | 'VC' 426 | | 'VE' 427 | | 'VG' 428 | | 'VI' 429 | | 'VN' 430 | | 'VU' 431 | | 'WF' 432 | | 'WS' 433 | | 'YE' 434 | | 'YT' 435 | | 'ZA' 436 | | 'ZM' 437 | | 'ZW' 438 | 439 | export type UktiDefinitionItemVariables = [Record] 440 | 441 | export type UktiDefinitionItem = undefined | UktiDefinitionItemVariables 442 | 443 | export type UktiDefinition = Record> 444 | 445 | export type UktiTranslationData = { 446 | [P in keyof Definition]: Definition[P] extends UktiDefinitionItemVariables 447 | ? string 448 | : undefined extends Definition[P] 449 | ? string 450 | : Definition[P] extends Record 451 | ? Record 452 | : never 453 | } 454 | 455 | type PartialDeep = { 456 | [P in keyof T]?: PartialDeep 457 | } 458 | 459 | export type UktiTranslationDataPartial = PartialDeep< 460 | UktiTranslationData 461 | > 462 | 463 | export type UktiTranslation< 464 | Definition extends UktiDefinition, 465 | Regions extends string = UktiRegions 466 | > = UktiTranslationData & { 467 | regions?: { 468 | [R in Regions]?: UktiTranslationDataPartial 469 | } 470 | } 471 | 472 | type UktiLanguageDefault = 'en' 473 | 474 | export type UktiTranslations< 475 | Definition extends UktiDefinition, 476 | Languages extends string = UktiLanguages, 477 | LanguageDefault extends string = UktiLanguageDefault, 478 | Regions extends string = UktiRegions 479 | > = Partial>> & 480 | Record> 481 | 482 | export type UktiTranslate = { 483 | [P in keyof Definition]: Definition[P] extends Record 484 | ? { 485 | [Q in keyof Definition[P]]: ( 486 | ...parameters: Definition[P][Q] extends UktiDefinitionItemVariables 487 | ? Definition[P][Q] 488 | : [] 489 | ) => string 490 | } 491 | : ( 492 | ...parameters: Definition[P] extends UktiDefinitionItemVariables ? Definition[P] : [] 493 | ) => string 494 | } 495 | 496 | export type UktiTranslator< 497 | Definition extends UktiDefinition, 498 | Languages extends string = UktiLanguages, 499 | Regions extends string = UktiRegions 500 | > = (language: Languages, region?: Regions) => UktiTranslate 501 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](https://github.com/romelperez/ukti/raw/main/ukti.png) 2 | 3 | # Ukti (उक्ति) 4 | 5 | [![version](https://img.shields.io/npm/v/ukti)](https://npmjs.org/package/ukti) 6 | [![tests](https://github.com/romelperez/ukti/workflows/tests/badge.svg)](https://github.com/romelperez/ukti/actions) 7 | [![codefactor](https://www.codefactor.io/repository/github/romelperez/ukti/badge)](https://www.codefactor.io/repository/github/romelperez/ukti) 8 | [![npm bundle size](https://img.shields.io/bundlephobia/minzip/ukti.svg)](https://bundlephobia.com/package/ukti) 9 | [![downloads](https://img.shields.io/npm/dm/ukti.svg)](https://npmjs.org/package/ukti) 10 | [![github stars](https://img.shields.io/github/stars/romelperez/ukti.svg?style=social&label=stars)](https://github.com/romelperez/ukti) 11 | [![license](https://img.shields.io/github/license/romelperez/ukti.svg)](https://github.com/romelperez/ukti/blob/main/LICENSE) 12 | 13 | ~1kB Type-safe i18n and l10n JavaScript utility. 14 | 15 | Ukti uses [ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) for language codes 16 | and [ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) for region codes 17 | by default. 18 | 19 | "Ukti" from Sanskrit "उक्ति" translates Speech or Language. 20 | 21 | ## Install 22 | 23 | For any ESM and CommonJS JavaScript environment. If TypeScript is used, version 4.5+ is required. 24 | 25 | ```bash 26 | npm i ukti 27 | ``` 28 | 29 | For UMD version: 30 | 31 | ```ts 32 | import { createUktiTranslator } from 'ukti/build/umd/ukti.umd.cjs' 33 | ``` 34 | 35 | ```html 36 |