├── .circleci └── config.yml ├── .gitignore ├── .node-version ├── .npmignore ├── .yarnclean ├── Makefile ├── README.md ├── __tests__ └── typed_i18n_test.re ├── bsconfig.json ├── example ├── .flowconfig ├── package.json ├── renovate.json ├── src │ ├── index.js │ ├── index.ts │ └── locale.json ├── tsconfig.json └── yarn.lock ├── fixture ├── locale.json ├── locale.translation.d.ts └── locale.translation.js.flow ├── index.js ├── package.json ├── renovate.json ├── src ├── translate.re ├── typed_i18n.re └── utils.re └── yarn.lock /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | references: 3 | docker_container_of_nodejs: &docker_container_of_nodejs 4 | docker: 5 | - image: circleci/node:8.12.0 6 | working_directory: ~/typed_i18n 7 | install_yarn: &install_yarn 8 | run: 9 | name: Install yarn 10 | command: sudo npm i -g yarn@1.7.0 11 | restore_cache: &restore_cache 12 | restore_cache: 13 | name: Restore yarn cache 14 | keys: 15 | - yarn-{{ checksum "yarn.lock" }} 16 | - yarn- 17 | install_node_modules: &install_node_modules 18 | run: 19 | name: Install dependencies 20 | command: | 21 | yarn install --prefer-offline 22 | yarn build 23 | cd example && yarn install --prefer-offline 24 | save_cache: &save_cache 25 | save_cache: 26 | name: Save yarn cache 27 | key: yarn-{{ checksum "yarn.lock" }} 28 | paths: 29 | - ~/.yarn 30 | - ~/.cache/yarn 31 | - ~/typed_i18n/node_modules 32 | persist: &persist 33 | persist_to_workspace: 34 | name: Persist to workspace 35 | root: /home/circleci/typed_i18n 36 | paths: 37 | - node_modules 38 | - example/node_modules 39 | - lib 40 | restore: &restore 41 | attach_workspace: 42 | name: Restore from workspace 43 | at: /home/circleci/typed_i18n 44 | jobs: 45 | install_dependencies: 46 | <<: *docker_container_of_nodejs 47 | steps: 48 | - checkout 49 | - *install_yarn 50 | - *restore_cache 51 | - *install_node_modules 52 | - *save_cache 53 | - *persist 54 | build: 55 | <<: *docker_container_of_nodejs 56 | steps: 57 | - checkout 58 | - *restore 59 | - run: 60 | name: Run tests 61 | command: yarn test 62 | npm_update: 63 | <<: *docker_container_of_nodejs 64 | steps: 65 | - checkout 66 | - *restore 67 | - run: 68 | name: Update 69 | command: | 70 | npx renovate --token "$GITHUB_ACCESS_TOKEN_RENOVATE" kogai/typed_i18n 71 | cd example && npx renovate --token "$GITHUB_ACCESS_TOKEN_RENOVATE" kogai/typed_i18n 72 | 73 | workflows: 74 | version: 2 75 | ordinary_workflow: 76 | jobs: 77 | - install_dependencies 78 | - build: 79 | requires: 80 | - install_dependencies 81 | update: 82 | triggers: 83 | - schedule: 84 | cron: "0 15 * * *" 85 | filters: 86 | branches: 87 | only: 88 | - master 89 | jobs: 90 | - install_dependencies 91 | - npm_update: 92 | requires: 93 | - install_dependencies 94 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.exe 2 | *.obj 3 | *.out 4 | *.compile 5 | *.native 6 | *.byte 7 | *.cmo 8 | *.annot 9 | *.cmi 10 | *.cmx 11 | *.cmt 12 | *.cmti 13 | *.cma 14 | *.a 15 | *.cmxa 16 | *.obj 17 | *~ 18 | *.annot 19 | *.cmj 20 | *.bak 21 | lib/bs 22 | *.mlast 23 | *.mliast 24 | .vscode 25 | .merlin 26 | !sandbox/ 27 | example/src/*.js.flow 28 | example/src/*.d.ts 29 | .merlin 30 | *.bs.js 31 | node_modules 32 | coverage 33 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 22.16.0 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .circleci/ 2 | .git/ 3 | .vscode/ 4 | example/ 5 | fixture/ 6 | node_modules/ 7 | lib/bs/ 8 | src/ 9 | .gitignore 10 | .merlin 11 | .yarnclean 12 | Makefile 13 | yarn-error.log 14 | -------------------------------------------------------------------------------- /.yarnclean: -------------------------------------------------------------------------------- 1 | # test directories 2 | __tests__ 3 | test 4 | tests 5 | powered-test 6 | 7 | # asset directories 8 | docs 9 | doc 10 | website 11 | images 12 | assets 13 | 14 | # examples 15 | example 16 | examples 17 | 18 | # code coverage directories 19 | coverage 20 | .nyc_output 21 | 22 | # build scripts 23 | Makefile 24 | Gulpfile.js 25 | Gruntfile.js 26 | 27 | # configs 28 | .tern-project 29 | .gitattributes 30 | .editorconfig 31 | .*ignore 32 | .eslintrc 33 | .jshintrc 34 | .flowconfig 35 | .documentup.json 36 | .yarn-metadata.json 37 | .*.yml 38 | *.yml 39 | 40 | # misc 41 | *.gz 42 | *.md 43 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | NAME := typed_i18n 2 | EXT := bs.js 3 | OCB := jbuilder 4 | DIST := _build/default/src 5 | SRC_FILES := $(shell find ./ -type f -name '*.re') 6 | SRC_FILES += package.json 7 | SRC_DIRS := "src" 8 | OPAM_VER := 4.03.0 9 | ARGS := -i fixture/locale.json -o fixture -p ja -l flow -l typescript 10 | NPM_BIN := $(shell npm bin) 11 | 12 | src/$(NAME).$(EXT): $(SRC_FILES) 13 | $(NPM_BIN)/bsb -make-world 14 | 15 | __tests__/$(NAME)_test.$(EXT): $(SRC_FILES) 16 | $(NPM_BIN)/bsb -make-world 17 | 18 | lib/bundle.$(EXT): src/$(NAME).$(EXT) 19 | $(NPM_BIN)/webpack src/$(NAME).$(EXT) -p -o lib/bundle.$(EXT) --target=node 20 | 21 | .PHONY: run 22 | run: 23 | yarn start -- $(ARGS) 24 | 25 | .PHONY: test 26 | test: 27 | cd example && \ 28 | yarn test 29 | 30 | .PHONY: publish 31 | publish: 32 | npm version patch 33 | npm publish --access public 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # typed_i18n 2 | 3 | [![npm version](https://badge.fury.io/js/%40kogai%2Ftyped_i18n.svg)](https://badge.fury.io/js/%40kogai%2Ftyped_i18n) 4 | [![CircleCI](https://circleci.com/gh/kogai/typed_i18n.svg?style=svg)](https://circleci.com/gh/kogai/typed_i18n) 5 | 6 | Generate strictly typed definition of TFunction from own [i18next](https://github.com/i18next/i18next) dictionary file. 7 | 8 | It generate from 9 | 10 | ```json 11 | { 12 | "en": { 13 | "translation": { 14 | "foo": { 15 | "bar": "some text", 16 | "buzz": 999 17 | } 18 | } 19 | } 20 | } 21 | ``` 22 | 23 | as 24 | 25 | ```javascript 26 | // @flow 27 | declare function t(_: "foo"): { 28 | +bar: string, 29 | +buzz: number 30 | }; 31 | declare function t(_: "foo.bar"): string; 32 | declare function t(_: "foo.buzz"): number; 33 | 34 | export type TFunction = typeof t; 35 | ``` 36 | 37 | or 38 | 39 | ```typescript 40 | declare namespace typed_i18n { 41 | interface TFunction { 42 | t(_: "foo"): { 43 | +bar: string, 44 | +buzz: number, 45 | }; 46 | t(_: "foo.bar"): string; 47 | t(_: "foo.buzz"): number; 48 | } 49 | } 50 | export = typed_i18n; 51 | ``` 52 | 53 | then if you use TFunction like below, type-checker warn you function call by invalid path 54 | 55 | ```javascript 56 | // @flow 57 | 58 | import type { TFunction } from "./locale.translation"; // Definition file generated 59 | 60 | declare var t: TFunction; 61 | 62 | // Those are ok 63 | const x: { bar: string, buzz: number } = t("foo"); 64 | const x1: string = x.bar; 65 | const x2: number = x.buzz; 66 | // Expect error 67 | const x3 = x.buzzz; 68 | 69 | // Expect error 70 | const y = t("fooo"); 71 | 72 | // Those are also strictly typed too 73 | const z1: string = t("foo.bar"); 74 | const z2: number = t("foo.buzz"); 75 | ``` 76 | 77 | ### Usage 78 | 79 | ```bash 80 | # Basic usage 81 | $ typed_i18n -i path/to/your.json -o path/to/out/dir 82 | 83 | # Support also typescript 84 | $ typed_i18n -i path/to/your.json -o path/to/out/dir -l typescript 85 | 86 | # You can specify namespaces instead of default "translation" 87 | $ typed_i18n -i path/to/your.json -o path/to/out/dir -n translate -n my-namespace -n other-namespace 88 | ``` 89 | -------------------------------------------------------------------------------- /__tests__/typed_i18n_test.re: -------------------------------------------------------------------------------- 1 | open Jest; 2 | 3 | open Expect; 4 | 5 | open! Expect.Operators; 6 | 7 | describe("Expect", () => 8 | Expect.(test("toBe", () => 9 | expect(1 + 2) |> toBe(3) 10 | )) 11 | ); 12 | 13 | describe("Expect.Operators", () => 14 | test("==", () => 15 | expect(1 + 2) === 3 16 | ) 17 | ); 18 | 19 | describe("Utils", () => { 20 | let json = 21 | Json.Encode.(object_([("a", string("a")), ("b", string("b"))])); 22 | test("find member", () => 23 | expect(Utils.member("a", json)) === Json.Encode.(string("a")) 24 | ); 25 | }); 26 | -------------------------------------------------------------------------------- /bsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typed_i18n", 3 | "version": "0.1.0", 4 | "sources": [{ 5 | "dir": "src" 6 | }, { 7 | "dir": "__tests__", 8 | "type": "dev" 9 | }], 10 | "package-specs": { 11 | "module": "commonjs", 12 | "in-source": true 13 | }, 14 | "suffix": ".bs.js", 15 | "warnings": { 16 | "number": "-44", 17 | "error": "+101" 18 | }, 19 | "refmt": 3, 20 | "bs-dependencies": [ 21 | "bs-cmdliner", 22 | "bs-easy-format" 23 | ], 24 | "bs-dev-dependencies": [ 25 | "@glennsl/bs-jest", 26 | "@glennsl/bs-json" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /example/.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | [include] 4 | 5 | [libs] 6 | 7 | [options] 8 | suppress_comment= \\(.\\|\n\\)*\\$ExpectError 9 | 10 | [lints] 11 | 12 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typed_i18n_sandbox", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "../index.js -i ./src/locale.json -o ./src -p ja -l flow -l typescript", 8 | "show-version": "../index.js --version", 9 | "test": "yarn show-version && yarn build && flow && tsc" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "devDependencies": {}, 15 | "dependencies": { 16 | "flow-bin": "0.126.1", 17 | "typescript": "4.9.5" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /example/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ], 5 | "prHourlyLimit": 0, 6 | "prConcurrentLimit": 0 7 | } 8 | -------------------------------------------------------------------------------- /example/src/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { TFunction } from './locale.translation'; 4 | declare var t: TFunction; 5 | 6 | // $ExpectError 7 | const x = t("invalid.string") 8 | const y = t("children.[1].first_name") 9 | const zs = t("body_copies") 10 | const can_receive_option = t("body_copies", {}) 11 | 12 | // $ExpectError 13 | zs.map(z => (z: number)) 14 | -------------------------------------------------------------------------------- /example/src/index.ts: -------------------------------------------------------------------------------- 1 | import { TFunction } from './locale.translation'; 2 | 3 | declare var t: TFunction; 4 | 5 | // @ts-ignore 6 | const x = t("invalid.string") 7 | const y = t("children.[1].first_name") 8 | const zs = t("body_copies") 9 | const can_receive_option = t("body_copies", {}) 10 | 11 | // @ts-ignore 12 | zs.map((z: number) => z) 13 | -------------------------------------------------------------------------------- /example/src/locale.json: -------------------------------------------------------------------------------- 1 | { 2 | "ja": { 3 | "translation": { 4 | "title": "タイトル", 5 | "body_copies": [ 6 | "これは", 7 | "本文", 8 | "です" 9 | ], 10 | "child": { 11 | "title_of_child": "サブタイトル", 12 | "variable_types": { 13 | "key_of_int": 999, 14 | "key_of_float": 999.999, 15 | "key_of_boolean": true, 16 | "key_of_null": null 17 | } 18 | }, 19 | "children": [{ 20 | "first_name": "太郎", 21 | "familly_name": "山田" 22 | }, { 23 | "first_name": "花子", 24 | "familly_name": "田中" 25 | }] 26 | } 27 | }, 28 | "en": { 29 | "translation": { 30 | "title": "Title", 31 | "body_copies": [ 32 | "This", 33 | "is", 34 | "body_copies" 35 | ] 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */ 5 | "module": "ESNext", /* Specify module code generation: 'none', commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 6 | // "lib": [], /* Specify library files to be included in the compilation: */ 7 | // "allowJs": true, /* Allow javascript files to be compiled. */ 8 | // "checkJs": true, /* Report errors in .js files. */ 9 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 10 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 11 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 12 | // "outFile": "./", /* Concatenate and emit output to single file. */ 13 | // "outDir": "./", /* Redirect output structure to the directory. */ 14 | "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 15 | // "removeComments": true, /* Do not emit comments to output. */ 16 | "noEmit": true, /* Do not emit outputs. */ 17 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 18 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 19 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 20 | 21 | /* Strict Type-Checking Options */ 22 | "strict": true, /* Enable all strict type-checking options. */ 23 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 24 | // "strictNullChecks": true, /* Enable strict null checks. */ 25 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 26 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 27 | 28 | /* Additional Checks */ 29 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 30 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 31 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 32 | // "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */ 33 | 34 | /* Module Resolution Options */ 35 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 36 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 37 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 38 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 39 | // "typeRoots": [], /* List of folders to include type definitions from. */ 40 | // "types": [], /* Type declaration files to be included in compilation. */ 41 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 42 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 43 | 44 | /* Source Map Options */ 45 | // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 46 | // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */ 47 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 48 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 49 | 50 | /* Experimental Options */ 51 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 52 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /example/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | flow-bin@0.126.1: 6 | version "0.126.1" 7 | resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.126.1.tgz#2726595e1891dc35b379b5994627432df4ead52c" 8 | integrity sha512-RI05x7rVzruRVJQN3M4vLEjZMwUHJKhGz9FmL8HN7WiSo66/131EyJS6Vo8PkKyM2pgT9GRWfGP/tXlqS54XUg== 9 | 10 | typescript@4.9.5: 11 | version "4.9.5" 12 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" 13 | integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== 14 | -------------------------------------------------------------------------------- /fixture/locale.json: -------------------------------------------------------------------------------- 1 | { 2 | "ja": { 3 | "translation": { 4 | "title": "タイトル", 5 | "body_copies": [ 6 | "これは", 7 | "本文", 8 | "です" 9 | ], 10 | "support_tuple": [ 11 | "タプル", 12 | 1, 13 | true 14 | ], 15 | "array_of_array": [ 16 | [1, 2], 17 | [3, 4] 18 | ], 19 | "tuple_of_tuple": [ 20 | ["5", true, { 21 | "foo": "bar", 22 | "bar": null 23 | }], 24 | [ 25 | "foo", true, 10 26 | ] 27 | ], 28 | "child": { 29 | "title_of_child": "サブタイトル", 30 | "variable_types": { 31 | "key_of_int": 999, 32 | "key_of_float": 999.999, 33 | "key_of_boolean": true, 34 | "key_of_null": null 35 | } 36 | }, 37 | "children": [{ 38 | "first_name": "太郎", 39 | "familly_name": "山田" 40 | }, { 41 | "first_name": "花子", 42 | "familly_name": "田中" 43 | }], 44 | "array_of_empty_object": [{}], 45 | "allow-comupted-properties": { 46 | "0": "数値", 47 | "foo-bar": true 48 | } 49 | } 50 | }, 51 | "en": { 52 | "translation": { 53 | "title": "Title", 54 | "body_copies": [ 55 | "This", 56 | "is", 57 | "body_copies" 58 | ] 59 | } 60 | }, 61 | "zh": { 62 | "translation": { 63 | "title": "标题", 64 | "support_tuple": [ 65 | "这是", 66 | "一句话", 67 | 100, 68 | null 69 | ] 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /fixture/locale.translation.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace typed_i18n { 2 | interface TFunction { 3 | (_: "title", __?: {}): string; (_: "body_copies", __?: {}): string[]; 4 | (_: "body_copies.[0]", __?: {}): string; 5 | (_: "body_copies.[1]", __?: {}): string; 6 | (_: "body_copies.[2]", __?: {}): string; 7 | (_: "support_tuple", __?: {}): [ string, number, boolean ]; 8 | (_: "support_tuple.[0]", __?: {}): string; 9 | (_: "support_tuple.[1]", __?: {}): number; 10 | (_: "support_tuple.[2]", __?: {}): boolean; 11 | (_: "array_of_array", __?: {}): number[][]; 12 | (_: "array_of_array.[0]", __?: {}): number[]; 13 | (_: "array_of_array.[0].[0]", __?: {}): number; 14 | (_: "array_of_array.[0].[1]", __?: {}): number; 15 | (_: "array_of_array.[1]", __?: {}): number[]; 16 | (_: "array_of_array.[1].[0]", __?: {}): number; 17 | (_: "array_of_array.[1].[1]", __?: {}): number; 18 | (_: "tuple_of_tuple", __?: {}): [ 19 | [ string, boolean, { readonly "foo": string, readonly "bar": null } ], 20 | [ string, boolean, number ] 21 | ]; 22 | (_: "tuple_of_tuple.[0]", __?: {}): [ string, boolean, { readonly "foo": string, readonly "bar": null } ]; 23 | (_: "tuple_of_tuple.[0].[0]", __?: {}): string; 24 | (_: "tuple_of_tuple.[0].[1]", __?: {}): boolean; 25 | (_: "tuple_of_tuple.[0].[2]", __?: {}): { readonly "foo": string, readonly "bar": null }; 26 | (_: "tuple_of_tuple.[0].[2].foo", __?: {}): string; 27 | (_: "tuple_of_tuple.[0].[2].bar", __?: {}): null; 28 | (_: "tuple_of_tuple.[1]", __?: {}): [ string, boolean, number ]; 29 | (_: "tuple_of_tuple.[1].[0]", __?: {}): string; 30 | (_: "tuple_of_tuple.[1].[1]", __?: {}): boolean; 31 | (_: "tuple_of_tuple.[1].[2]", __?: {}): number; 32 | (_: "child", __?: {}): { 33 | readonly "title_of_child": string, 34 | readonly "variable_types": { 35 | readonly "key_of_int": number, 36 | readonly "key_of_float": number, 37 | readonly "key_of_boolean": boolean, 38 | readonly "key_of_null": null 39 | } 40 | }; 41 | (_: "child.title_of_child", __?: {}): string; 42 | (_: "child.variable_types", __?: {}): { 43 | readonly "key_of_int": number, 44 | readonly "key_of_float": number, 45 | readonly "key_of_boolean": boolean, 46 | readonly "key_of_null": null 47 | }; 48 | (_: "child.variable_types.key_of_int", __?: {}): number; 49 | (_: "child.variable_types.key_of_float", __?: {}): number; 50 | (_: "child.variable_types.key_of_boolean", __?: {}): boolean; 51 | (_: "child.variable_types.key_of_null", __?: {}): null; 52 | (_: "children", __?: {}): { readonly "first_name": string, readonly "familly_name": string }[]; 53 | (_: "children.[0]", __?: {}): { readonly "first_name": string, readonly "familly_name": string }; 54 | (_: "children.[0].first_name", __?: {}): string; 55 | (_: "children.[0].familly_name", __?: {}): string; 56 | (_: "children.[1]", __?: {}): { readonly "first_name": string, readonly "familly_name": string }; 57 | (_: "children.[1].first_name", __?: {}): string; 58 | (_: "children.[1].familly_name", __?: {}): string; 59 | (_: "array_of_empty_object", __?: {}): {}[]; 60 | (_: "array_of_empty_object.[0]", __?: {}): {}; 61 | (_: "allow-comupted-properties", __?: {}): { readonly "0": string, readonly "foo-bar": boolean }; 62 | (_: "allow-comupted-properties.0", __?: {}): string; 63 | (_: "allow-comupted-properties.foo-bar", __?: {}): boolean 64 | } 65 | } 66 | export = typed_i18n; 67 | -------------------------------------------------------------------------------- /fixture/locale.translation.js.flow: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | declare function t(_: "title", _?: {}): string; 4 | declare function t(_: "body_copies", _?: {}): string[]; 5 | declare function t(_: "body_copies.[0]", _?: {}): string; 6 | declare function t(_: "body_copies.[1]", _?: {}): string; 7 | declare function t(_: "body_copies.[2]", _?: {}): string; 8 | declare function t(_: "support_tuple", _?: {}): [ string, number, boolean ]; 9 | declare function t(_: "support_tuple.[0]", _?: {}): string; 10 | declare function t(_: "support_tuple.[1]", _?: {}): number; 11 | declare function t(_: "support_tuple.[2]", _?: {}): boolean; 12 | declare function t(_: "array_of_array", _?: {}): number[][]; 13 | declare function t(_: "array_of_array.[0]", _?: {}): number[]; 14 | declare function t(_: "array_of_array.[0].[0]", _?: {}): number; 15 | declare function t(_: "array_of_array.[0].[1]", _?: {}): number; 16 | declare function t(_: "array_of_array.[1]", _?: {}): number[]; 17 | declare function t(_: "array_of_array.[1].[0]", _?: {}): number; 18 | declare function t(_: "array_of_array.[1].[1]", _?: {}): number; 19 | declare function t(_: "tuple_of_tuple", _?: {}): [ 20 | [ string, boolean, { +"foo": string, +"bar": null } ], 21 | [ string, boolean, number ] 22 | ]; 23 | declare function t(_: "tuple_of_tuple.[0]", _?: {}): [ string, boolean, { +"foo": string, +"bar": null } ]; 24 | declare function t(_: "tuple_of_tuple.[0].[0]", _?: {}): string; 25 | declare function t(_: "tuple_of_tuple.[0].[1]", _?: {}): boolean; 26 | declare function t(_: "tuple_of_tuple.[0].[2]", _?: {}): { +"foo": string, +"bar": null }; 27 | declare function t(_: "tuple_of_tuple.[0].[2].foo", _?: {}): string; 28 | declare function t(_: "tuple_of_tuple.[0].[2].bar", _?: {}): null; 29 | declare function t(_: "tuple_of_tuple.[1]", _?: {}): [ string, boolean, number ]; 30 | declare function t(_: "tuple_of_tuple.[1].[0]", _?: {}): string; 31 | declare function t(_: "tuple_of_tuple.[1].[1]", _?: {}): boolean; 32 | declare function t(_: "tuple_of_tuple.[1].[2]", _?: {}): number; 33 | declare function t(_: "child", _?: {}): { 34 | +"title_of_child": string, 35 | +"variable_types": { 36 | +"key_of_int": number, 37 | +"key_of_float": number, 38 | +"key_of_boolean": boolean, 39 | +"key_of_null": null 40 | } 41 | }; 42 | declare function t(_: "child.title_of_child", _?: {}): string; 43 | declare function t(_: "child.variable_types", _?: {}): { 44 | +"key_of_int": number, 45 | +"key_of_float": number, 46 | +"key_of_boolean": boolean, 47 | +"key_of_null": null 48 | }; 49 | declare function t(_: "child.variable_types.key_of_int", _?: {}): number; 50 | declare function t(_: "child.variable_types.key_of_float", _?: {}): number; 51 | declare function t(_: "child.variable_types.key_of_boolean", _?: {}): boolean; 52 | declare function t(_: "child.variable_types.key_of_null", _?: {}): null; 53 | declare function t(_: "children", _?: {}): { +"first_name": string, +"familly_name": string }[]; 54 | declare function t(_: "children.[0]", _?: {}): { +"first_name": string, +"familly_name": string }; 55 | declare function t(_: "children.[0].first_name", _?: {}): string; 56 | declare function t(_: "children.[0].familly_name", _?: {}): string; 57 | declare function t(_: "children.[1]", _?: {}): { +"first_name": string, +"familly_name": string }; 58 | declare function t(_: "children.[1].first_name", _?: {}): string; 59 | declare function t(_: "children.[1].familly_name", _?: {}): string; 60 | declare function t(_: "array_of_empty_object", _?: {}): {}[]; 61 | declare function t(_: "array_of_empty_object.[0]", _?: {}): {}; 62 | declare function t(_: "allow-comupted-properties", _?: {}): { +"0": string, +"foo-bar": boolean }; 63 | declare function t(_: "allow-comupted-properties.0", _?: {}): string; 64 | declare function t(_: "allow-comupted-properties.foo-bar", _?: {}): boolean; 65 | 66 | export type TFunction = typeof t 67 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | const path = require('path'); 6 | const spawn = require('child_process').spawn; 7 | 8 | const args = process.argv.slice(2); 9 | const bin = path.join(__dirname, 'lib/bundle.bs.js'); 10 | 11 | spawn("node", [bin].concat(args), { 12 | stdio: 'inherit' 13 | }) 14 | .on('exit', process.exit); 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@kogai/typed_i18n", 3 | "version": "0.6.0", 4 | "description": "Type safe i18n with Flow and TypeScript", 5 | "bin": "index.js", 6 | "scripts": { 7 | "clean": "bsb -clean-world && rm -f lib/bundle.bs.js", 8 | "build": "make src/typed_i18n.bs.js", 9 | "build:production": "make lib/bundle.bs.js", 10 | "watch": "bsb -make-world -w", 11 | "test": "yarn test:unit && cd example && yarn test", 12 | "test:unit": "make __tests__/typed_i18n_test.bs.js && yarn jest", 13 | "test:unit:watch": "yarn jest --watch", 14 | "start": "yarn build && node src/typed_i18n.bs.js", 15 | "start:build": "yarn start -- -i fixture/locale.json -o fixture -p ja -l flow -l typescript", 16 | "prepublish": "yarn clean && yarn build:production" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/kogai/typed_i18n.git" 21 | }, 22 | "keywords": [ 23 | "BuckleScript", 24 | "flow", 25 | "typescript", 26 | "ocaml", 27 | "reasonml", 28 | "i18n" 29 | ], 30 | "author": "Shinichi Kogai", 31 | "license": "MIT", 32 | "bugs": { 33 | "url": "https://github.com/kogai/typed_i18n/issues" 34 | }, 35 | "homepage": "https://github.com/kogai/typed_i18n#readme", 36 | "devDependencies": { 37 | "@glennsl/bs-jest": "0.4.6", 38 | "@glennsl/bs-json": "5.0.4", 39 | "bs-cmdliner": "0.1.0", 40 | "bs-easy-format": "0.1.0", 41 | "bs-platform": "7.3.1", 42 | "webpack": "4.47.0", 43 | "webpack-cli": "3.3.12" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ], 5 | "prHourlyLimit": 0, 6 | "prConcurrentLimit": 0, 7 | "automerge": true 8 | } 9 | -------------------------------------------------------------------------------- /src/translate.re: -------------------------------------------------------------------------------- 1 | exception Unreachable; 2 | 3 | exception Invalid_extension(option(string)); 4 | 5 | module type Translatable = { 6 | type t; 7 | let extension: string; 8 | let read_only_tag: option(string); 9 | let definition: (Js.Json.t => Easy_format.t, t) => Easy_format.t; 10 | let definitions: list(Easy_format.t) => string; 11 | }; 12 | 13 | module Translator = 14 | (Impl: Translatable) 15 | : ( 16 | { 17 | type t; 18 | let format: Js.Json.t => Easy_format.t; 19 | let definition: t => Easy_format.t; 20 | let output_filename: (string, string) => string; 21 | let definitions: list(Easy_format.t) => string; 22 | } with 23 | type t = Impl.t 24 | ) => { 25 | open Easy_format; 26 | type t = Impl.t; 27 | let rec format = json => 28 | switch (Js.Json.classify(json)) { 29 | | Js.Json.JSONObject(xs) => 30 | let es = Js.Dict.entries(xs); 31 | if (Array.length(es) == 0) { 32 | Atom("{}", atom); 33 | } else { 34 | es 35 | |> Array.map(format_field) 36 | |> (xs => List(("{", ",", "}", list), Array.to_list(xs))); 37 | }; 38 | | Js.Json.JSONArray(xs) => format_list(Array.to_list(xs)) 39 | | Js.Json.JSONFalse 40 | | Js.Json.JSONTrue => Atom("boolean", atom) 41 | | Js.Json.JSONNumber(_) => Atom("number", atom) 42 | | Js.Json.JSONString(_) => Atom("string", atom) 43 | | Js.Json.JSONNull => Atom("null", atom) 44 | } 45 | and format_field = ((key, value)) => { 46 | let read_only_tag = 47 | switch (Impl.read_only_tag) { 48 | | Some(s) => s 49 | | None => "" 50 | }; 51 | let prop = Format.sprintf("%s\"%s\":", read_only_tag, key); 52 | Label((Atom(prop, atom), label), format(value)); 53 | } 54 | and format_list = xs => { 55 | let array_or_tuple = List.map(format, xs); 56 | if (is_array(array_or_tuple)) { 57 | switch (array_or_tuple) { 58 | | [] => raise(Unreachable) 59 | | [x, ..._] => Atom(Pretty.to_string(x) ++ "[]", atom) 60 | }; 61 | } else { 62 | List(("[", ",", "]", list), array_or_tuple); 63 | }; 64 | } 65 | and is_array = 66 | fun 67 | | [] => true 68 | | [x, ...xs] => 69 | xs 70 | |> List.fold_left( 71 | ((before, is_array'), next) => ( 72 | next, 73 | is_array' && before == next, 74 | ), 75 | (x, true), 76 | ) 77 | |> (((_, x)) => x); 78 | let definition = Impl.definition(format); 79 | let definitions = Impl.definitions; 80 | let output_filename = (path, namespace) => { 81 | let filename = Filename.basename(path); 82 | switch (Filename.chop_extension(filename)) { 83 | | name => Format.sprintf("%s.%s.%s", name, namespace, Impl.extension) 84 | | exception _ => raise(Invalid_extension(None)) 85 | }; 86 | }; 87 | }; 88 | -------------------------------------------------------------------------------- /src/typed_i18n.re: -------------------------------------------------------------------------------- 1 | open Easy_format; 2 | 3 | [@bs.module] 4 | external pkg : { 5 | . 6 | "version": string, 7 | "name": string, 8 | } = 9 | "../package.json"; 10 | 11 | type ty = { 12 | path: string, 13 | value: Js.Json.t, 14 | }; 15 | 16 | exception Unreachable; 17 | 18 | exception Invalid_namespace_key(string); 19 | 20 | exception Invalid_language_key(option(string)); 21 | 22 | exception Invalid_target_language(string); 23 | 24 | module Flow = { 25 | type t = ty; 26 | let extension = "js.flow"; 27 | let read_only_tag = Some("+"); 28 | let definition = (format, {path, value}) => { 29 | let fname = "t"; 30 | let typedef = Easy_format.Pretty.to_string @@ format(value); 31 | let result = 32 | Format.sprintf( 33 | "declare function %s(_: \"%s\", _?: {}): %s;", 34 | fname, 35 | path, 36 | typedef, 37 | ); 38 | Atom(result, atom); 39 | }; 40 | let definitions = contents => 41 | contents 42 | |> List.map(Easy_format.Pretty.to_string) 43 | |> String.concat("\n") 44 | |> Format.sprintf("// @flow\n\n%s\n\nexport type TFunction = typeof t\n"); 45 | }; 46 | 47 | module Typescript = { 48 | type t = ty; 49 | let extension = "d.ts"; 50 | let read_only_tag = Some("readonly "); 51 | let definition = (format, {path, value}) => { 52 | let typedef = Easy_format.Pretty.to_string @@ format(value); 53 | Atom(Format.sprintf("(_: \"%s\", __?: {}): %s", path, typedef), atom); 54 | }; 55 | let definitions = contents => { 56 | let methods = List(("interface TFunction {", ";", "}", list), contents); 57 | let interface = Atom(Easy_format.Pretty.to_string(methods), atom); 58 | let ns = 59 | List(("declare namespace typed_i18n {", "", "}", list), [interface]); 60 | Easy_format.Pretty.to_string(ns) ++ "\nexport = typed_i18n;\n"; 61 | }; 62 | }; 63 | 64 | let create_translator = 65 | fun 66 | | "flow" => ((module Flow): (module Translate.Translatable with type t = ty)) 67 | | "typescript" => ( 68 | (module Typescript): (module Translate.Translatable with type t = ty) 69 | ) 70 | | lang => raise @@ Invalid_target_language(lang); 71 | 72 | let insert_dot = (k1, k2) => 73 | if (k1 == "") { 74 | k2; 75 | } else { 76 | k1 ++ "." ++ k2; 77 | }; 78 | 79 | /* json -> t list */ 80 | let rec walk = (~path="", ~current_depth=0, ~max_depth, value) => { 81 | switch (Js.Json.classify(value)) { 82 | | _ when current_depth >= (max_depth + 1) => [] 83 | | Js.Json.JSONObject(xs) => 84 | let current = {path, value}; 85 | let next_depth = current_depth + 1; 86 | let children = 87 | List.fold_left( 88 | (acc, (k, v)) => 89 | List.append(acc, walk(~path=insert_dot(path, k), ~current_depth=next_depth, ~max_depth, v)), 90 | [], 91 | xs |> Js.Dict.entries |> Array.to_list, 92 | ); 93 | path == "" ? children : [current, ...children]; 94 | | Js.Json.JSONArray(xs) => 95 | let current = {path, value}; 96 | let next_depth = current_depth + 1; 97 | let children = 98 | List.fold_left( 99 | (acc, (i, x)) => { 100 | let path = 101 | insert_dot(path, Format.sprintf("[%s]", string_of_int(i))); 102 | List.append(acc, walk(~path, ~current_depth=next_depth, ~max_depth, x)); 103 | }, 104 | [], 105 | xs |> Array.to_list |> Utils.with_idx, 106 | ); 107 | [current, ...children]; 108 | | _ => [{path, value}] 109 | } 110 | }; 111 | 112 | module Logger: { 113 | type t = [ | `Warn | `Error | `Info]; 114 | let log: (t, format('a, out_channel, unit)) => 'a; 115 | } = { 116 | type t = [ | `Warn | `Error | `Info]; 117 | let log = (level, msg) => { 118 | switch (level) { 119 | | `Info => Printf.printf("\027[1;32m[INFO]: \027[0m") 120 | | `Warn => Printf.printf("\027[1;33m[WARN]: \027[0m") 121 | | `Error => Printf.printf("\027[1;31m[ERROR]: \027[0m") 122 | }; 123 | Printf.printf(msg); 124 | }; 125 | }; 126 | 127 | module Compatible: { 128 | let find: (Js.Json.t, string) => Js.Json.t; 129 | let correct: (~max_depth: int, Js.Json.t, Js.Json.t) => list(string); 130 | let check: (~max_depth: int, (string, Js.Json.t), (string, Js.Json.t)) => unit; 131 | } = { 132 | let rec find = (tree, key) => 133 | switch (Belt.List.fromArray(Js.String.split(".", key))) { 134 | | [] 135 | | [""] => raise(Unreachable) 136 | | [k] => 137 | let r = Js.Re.fromString("^\\[([0-9])\\]$"); 138 | switch (Js.String.match(r, k)) { 139 | | Some(rs) => 140 | let idx = 141 | switch (Belt.Array.get(rs, 1)) { 142 | | Some(i) => i 143 | | _ => "0" 144 | }; 145 | Utils.member(idx, tree); 146 | | None => Utils.member(key, tree) 147 | }; 148 | | [k, ...ks] => 149 | find( 150 | Utils.member(k, tree), 151 | Js.Array.joinWith(".", Belt.List.toArray(ks)), 152 | ) 153 | }; 154 | let correct = (~max_depth, primary, secondary) => { 155 | let impl = create_translator("flow"); 156 | module Impl = (val impl); 157 | module M = Translate.Translator(Impl); 158 | let type_of_json = json => 159 | json |> M.format |> Easy_format.Pretty.to_string; 160 | primary 161 | |> walk(~max_depth) 162 | |> List.map(({path, value}) => { 163 | let is_match = 164 | try (type_of_json(find(secondary, path)) == type_of_json(value)) { 165 | | Not_found => false 166 | }; 167 | (path, is_match); 168 | }) 169 | |> List.filter(((_, is_match)) => ! is_match) 170 | |> List.map(((x, _)) => x); 171 | }; 172 | let check = (~max_depth, (p_lang, p_json), (s_lang, s_json)) => 173 | correct(~max_depth, p_json, s_json) 174 | |> ( 175 | errors => { 176 | if (List.length(errors) > 0) { 177 | Logger.log( 178 | `Warn, 179 | "[%s] and [%s] are not compatible\n", 180 | p_lang, 181 | s_lang, 182 | ); 183 | }; 184 | errors; 185 | } 186 | ) 187 | |> ( 188 | errors => 189 | switch (Belt.List.splitAt(errors, 10)) { 190 | | Some((xs, ys)) => 191 | List.iter( 192 | path => Logger.log(`Warn, "[%s] isn't compatible\n", path), 193 | xs, 194 | ); 195 | Logger.log( 196 | `Warn, 197 | "And there are [%i] imcompatibles left\n", 198 | List.length(ys), 199 | ); 200 | | None => 201 | List.iter( 202 | path => Logger.log(`Warn, "[%s] isn't compatible\n", path), 203 | errors, 204 | ) 205 | } 206 | ); 207 | }; 208 | 209 | let check_compatibility = (~max_depth: int) => 210 | fun 211 | | [] => raise @@ Invalid_language_key(None) 212 | | [(primary_lang, primary_json), ...rest_langs] => 213 | List.iter( 214 | ((other_lang, other_json)) => 215 | Compatible.check( 216 | ~max_depth, 217 | (primary_lang, primary_json), 218 | (other_lang, other_json), 219 | ), 220 | rest_langs, 221 | ); 222 | 223 | let handle_language = 224 | (~max_depth: int, prefer_lang: string, namespaces: list(string), json: Js.Json.t) => { 225 | let gather_langs = 226 | Js.Json.( 227 | fun 228 | | JSONObject(x) => Belt.List.fromArray(Js.Dict.entries(x)) 229 | | _ => [] 230 | ); 231 | let languages = gather_langs(Js.Json.classify(json)); 232 | check_compatibility(~max_depth, languages); 233 | languages 234 | |> Utils.find(((l, _)) => l == prefer_lang) 235 | |> ( 236 | fun 237 | | Some((_, json)) => 238 | List.map( 239 | name => { 240 | let json = 241 | try (Utils.member(name, json)) { 242 | | Not_found => raise @@ Invalid_namespace_key(name) 243 | }; 244 | (name, walk(json, ~max_depth)); 245 | }, 246 | namespaces, 247 | ) 248 | | _ => raise(Unreachable) 249 | ); 250 | }; 251 | 252 | let translate = 253 | (~input_file, ~output_dir, ~languages, (namespace, path_and_values)) => 254 | List.iter( 255 | lang => { 256 | let impl = create_translator(lang); 257 | module Impl = (val impl); 258 | module M = Translate.Translator(Impl); 259 | path_and_values 260 | |> List.map(M.definition) 261 | |> M.definitions 262 | |> ( 263 | content => { 264 | let dist = 265 | output_dir ++ "/" ++ M.output_filename(input_file, namespace); 266 | Node.Fs.writeFileSync(dist, content, `utf8); 267 | Logger.log(`Info, "Generated %s\n", dist); 268 | } 269 | ); 270 | }, 271 | languages, 272 | ); 273 | 274 | module Cmd: { 275 | let name: string; 276 | let version: string; 277 | let run: (string, string, string, list(string), list(string), int) => unit; 278 | let term: Cmdliner.Term.t(unit); 279 | } = { 280 | open Cmdliner; 281 | let name = pkg##name; 282 | let version = "Version: " ++ pkg##version; 283 | let input = { 284 | let doc = "Path of source locale file"; 285 | Arg.( 286 | value & opt(string, "") & info(["i", "input"], ~docv="INPUT", ~doc) 287 | ); 288 | }; 289 | let output = { 290 | let doc = "Directory of output distination"; 291 | Arg.( 292 | value & opt(string, "") & info(["o", "output"], ~docv="OUTPUT", ~doc) 293 | ); 294 | }; 295 | let prefer = { 296 | let doc = "Preferred language"; 297 | Arg.( 298 | value 299 | & opt(string, "en") 300 | & info(["p", "prefer"], ~docv="PREFER", ~doc) 301 | ); 302 | }; 303 | let namespaces = { 304 | let doc = "List of namespace declared in locale file"; 305 | Arg.( 306 | value 307 | & opt_all(string, ["translation"]) 308 | & info(["n", "namespaces"], ~docv="NAMESPACES", ~doc) 309 | ); 310 | }; 311 | let languages = { 312 | let doc = "Destination language like flow or typescript"; 313 | Arg.( 314 | value 315 | & opt_all(string, ["flow"]) 316 | & info(["l", "languages"], ~docv="LANGUAGES", ~doc) 317 | ); 318 | }; 319 | let max_depth = { 320 | let doc = "Max depth of dictionary JSON tree"; 321 | Arg.( 322 | value 323 | & opt(int, 255) 324 | & info(["d", "max_depth"], ~docv="MAX_DEPTH", ~doc) 325 | ); 326 | }; 327 | let run = (input_file, output_dir, prefer, namespaces, languages, max_depth) => 328 | try ( 329 | input_file 330 | |> Node.Fs.readFileSync(_, `utf8) 331 | |> Js.Json.parseExn 332 | |> handle_language(~max_depth, prefer, namespaces) 333 | |> List.iter(translate(~input_file, ~output_dir, ~languages)) 334 | ) { 335 | | Invalid_namespace_key(key) => 336 | Logger.log(`Error, "Invalid namespace [%s] designated\n", key) 337 | | Invalid_language_key(None) => 338 | Logger.log(`Error, "Language key isn't existed\n") 339 | | Invalid_language_key(Some(lang)) => 340 | Logger.log(`Error, "Invalid language, [%s] isn't supported\n", lang) 341 | | Invalid_target_language(lang) => 342 | Logger.log(`Error, "Invalid target, [%s] isn't supported\n", lang) 343 | | Translate.Invalid_extension(Some(ext)) => 344 | Logger.log(`Error, "Invalid extension, [%s] isn't supported\n", ext) 345 | | Translate.Invalid_extension(None) => 346 | Logger.log(`Error, "Extention doesn't existed\n") 347 | | Js.Exn.Error(err) => 348 | let msg = 349 | switch (Js.Exn.message(err)) { 350 | | Some(m) => m 351 | | None => "UnKnown" 352 | }; 353 | Logger.log(`Error, "Invalid JSON \n%s\n", msg); 354 | | e => 355 | Logger.log(`Error, "Unhandled error occured\n"); 356 | raise(e); 357 | }; 358 | let term = 359 | Term.(const(run) $ input $ output $ prefer $ namespaces $ languages $ max_depth); 360 | }; 361 | 362 | let () = { 363 | let argv = 364 | Sys.argv 365 | |> Belt.List.fromArray 366 | |> Belt.List.tail 367 | |> ( 368 | fun 369 | | Some(xs) => Belt.List.toArray(xs) 370 | | _ => [||] 371 | ); 372 | Cmdliner.( 373 | Term.exit @@ 374 | Term.eval((Cmd.term, Term.info(Cmd.name, ~version=Cmd.version)), ~argv) 375 | ); 376 | }; 377 | -------------------------------------------------------------------------------- /src/utils.re: -------------------------------------------------------------------------------- 1 | let rec range = (from, to_) => 2 | if (from > to_) { 3 | []; 4 | } else { 5 | [from, ...range(from + 1, to_)]; 6 | }; 7 | 8 | let with_idx = xs => List.combine(range(0, List.length(xs) - 1), xs); 9 | 10 | let find = (p, xs) => 11 | try (Some(List.find(p, xs))) { 12 | | Not_found => None 13 | }; 14 | 15 | let rec last_exn = 16 | fun 17 | | [] => raise(Not_found) 18 | | [x] => x 19 | | [_, ...xs] => last_exn(xs); 20 | 21 | let member = (key: string, json: Js.Json.t) : Js.Json.t => { 22 | let mem = k => 23 | fun 24 | | Js.Json.JSONObject(xs) => 25 | switch (Js.Dict.get(xs, k)) { 26 | | Some(x) => x 27 | | None => raise(Not_found) 28 | } 29 | | Js.Json.JSONArray(xs) => 30 | try (xs[int_of_string(k)]) { 31 | | Invalid_argument(_) => raise(Not_found) 32 | } 33 | | JSONFalse => Js.Json.boolean(false) 34 | | JSONTrue => Js.Json.boolean(true) 35 | | JSONNull => Js.Json.null 36 | | JSONNumber(x) => Js.Json.number(x) 37 | | JSONString(x) => Js.Json.string(x); 38 | mem(key, Js.Json.classify(json)); 39 | }; 40 | --------------------------------------------------------------------------------