Repository: akameco/extract-react-intl-messages Branch: master Commit: 5e8f220414fe Files: 66 Total size: 61.9 KB Directory structure: gitextract_9ar36m_8/ ├── .all-contributorsrc ├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitattributes ├── .github/ │ ├── ISSUE_TEMPLATE.md │ ├── PULL_REQUEST_TEMPLATE.md │ └── workflows/ │ └── test.yml ├── .gitignore ├── .husky/ │ └── pre-commit ├── .prettierignore ├── .prettierrc ├── CODE_OF_CONDUCT.md ├── eslint.config.mjs ├── example/ │ ├── basic/ │ │ ├── babel.config.js │ │ ├── i18n/ │ │ │ ├── .keep │ │ │ ├── en.json │ │ │ └── ja.json │ │ ├── package.json │ │ └── src/ │ │ ├── App.jsx │ │ └── messages.js │ └── with-typescript/ │ ├── babel.config.js │ ├── i18n/ │ │ ├── en.json │ │ └── ja.json │ ├── package.json │ └── src/ │ ├── App.tsx │ └── messages.ts ├── jest.config.js ├── license ├── package.json ├── readme.md ├── src/ │ ├── cli.ts │ ├── extract-react-intl/ │ │ ├── index.ts │ │ ├── readme.md │ │ └── test/ │ │ ├── __snapshots__/ │ │ │ └── test.ts.snap │ │ ├── fixtures/ │ │ │ ├── .babelrc │ │ │ ├── components/ │ │ │ │ ├── App/ │ │ │ │ │ ├── index.js │ │ │ │ │ └── messages.js │ │ │ │ ├── Greeting/ │ │ │ │ │ ├── index.js │ │ │ │ │ └── messages.js │ │ │ │ └── LanguageProvider/ │ │ │ │ └── index.js │ │ │ ├── index.html │ │ │ └── index.js │ │ ├── pluginOrdering/ │ │ │ ├── .babelrc │ │ │ └── messages.js │ │ ├── resolution/ │ │ │ ├── .babelrc │ │ │ └── messages.js │ │ └── test.ts │ ├── global.d.ts │ ├── index.ts │ └── test/ │ ├── fixtures/ │ │ ├── custom/ │ │ │ ├── a/ │ │ │ │ └── messages.js │ │ │ ├── b/ │ │ │ │ └── messages.js │ │ │ └── i18n.js │ │ ├── default/ │ │ │ ├── a/ │ │ │ │ ├── App.js │ │ │ │ └── messages.js │ │ │ └── b/ │ │ │ └── messages.js │ │ ├── removed/ │ │ │ ├── a/ │ │ │ │ └── messages.js │ │ │ └── b/ │ │ │ └── messages.js │ │ └── unsorted/ │ │ ├── a/ │ │ │ └── messages.js │ │ └── b/ │ │ └── messages.js │ ├── json/ │ │ ├── __snapshots__/ │ │ │ └── test.ts.snap │ │ └── test.ts │ ├── test.ts │ └── yaml/ │ ├── __snapshots__/ │ │ └── test.ts.snap │ └── test.ts └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .all-contributorsrc ================================================ { "projectName": "extract-react-intl-messages", "projectOwner": "akameco", "files": ["readme.md"], "imageSize": 100, "commit": true, "contributors": [ { "login": "akameco", "name": "akameco", "avatar_url": "https://avatars2.githubusercontent.com/u/4002137?v=4", "profile": "http://akameco.github.io", "contributions": ["code", "test", "doc", "infra"] }, { "login": "hoantran-it", "name": "Hoan Tran", "avatar_url": "https://avatars3.githubusercontent.com/u/13161875?v=4", "profile": "http://hoantran.info", "contributions": ["code", "test"] }, { "login": "giantpinkwalrus", "name": "giantpinkwalrus", "avatar_url": "https://avatars1.githubusercontent.com/u/3383240?v=4", "profile": "https://github.com/giantpinkwalrus", "contributions": ["code"] }, { "login": "enrique-ramirez", "name": "enrique-ramirez", "avatar_url": "https://avatars3.githubusercontent.com/u/1190640?v=4", "profile": "https://github.com/enrique-ramirez", "contributions": ["doc"] }, { "login": "hoschi", "name": "Stefan Gojan", "avatar_url": "https://avatars2.githubusercontent.com/u/163128?v=4", "profile": "http://stefan-gojan.de", "contributions": ["bug", "code", "test"] }, { "login": "solomon23", "name": "Solomon English", "avatar_url": "https://avatars1.githubusercontent.com/u/857744?v=4", "profile": "https://lithe.net", "contributions": ["code"] }, { "login": "Filson14", "name": "Filip \"Filson\" Pasternak", "avatar_url": "https://avatars1.githubusercontent.com/u/4540538?v=4", "profile": "https://github.com/Filson14", "contributions": ["code"] }, { "login": "nodaguti", "name": "nodaguti", "avatar_url": "https://avatars0.githubusercontent.com/u/27622?v=4", "profile": "http://about.me/nodaguti", "contributions": ["code", "test"] }, { "login": "fix-fix", "name": "fix-fix", "avatar_url": "https://avatars1.githubusercontent.com/u/11943024?v=4", "profile": "https://github.com/fix-fix", "contributions": ["code"] }, { "login": "bradbarrow", "name": "bradbarrow", "avatar_url": "https://avatars3.githubusercontent.com/u/1264276?v=4", "profile": "http://bradbarrow.com", "contributions": ["bug", "code", "test"] }, { "login": "gmaclennan", "name": "Gregor MacLennan", "avatar_url": "https://avatars1.githubusercontent.com/u/290457?v=4", "profile": "http://ddem.us/", "contributions": ["code"] }, { "login": "zarv1k", "name": "Dmitry Zarva", "avatar_url": "https://avatars1.githubusercontent.com/u/6296643?v=4", "profile": "https://github.com/zarv1k", "contributions": ["code"] }, { "login": "panpanc", "name": "Michael Pan", "avatar_url": "https://avatars2.githubusercontent.com/u/29132669?v=4", "profile": "https://github.com/panpanc", "contributions": ["example"] }, { "login": "testower", "name": "Tom Erik Støwer", "avatar_url": "https://avatars2.githubusercontent.com/u/231492?v=4", "profile": "https://github.com/testower", "contributions": ["code"] }, { "login": "lensbart", "name": "Bart Lens", "avatar_url": "https://avatars0.githubusercontent.com/u/20876627?v=4", "profile": "https://nextbook.io", "contributions": ["code"] }, { "login": "revskill10", "name": "Truong Hoang Dung", "avatar_url": "https://avatars3.githubusercontent.com/u/1390196?v=4", "profile": "https://github.com/revskill10", "contributions": ["example"] }, { "login": "Nestoro", "name": "Nestoro", "avatar_url": "https://avatars.githubusercontent.com/u/13397845?v=4", "profile": "https://github.com/Nestoro", "contributions": ["code"] }, { "login": "lightnet328", "name": "Yutaro Kido", "avatar_url": "https://avatars.githubusercontent.com/u/2351326?v=4", "profile": "https://lightnet328.com/", "contributions": ["code"] } ], "repoType": "github", "commitConvention": "none", "repoHost": "https://github.com", "skipCi": true, "contributorsPerLine": 7 } ================================================ FILE: .babelrc ================================================ { "presets": [ [ "@babel/preset-env", { "targets": { "node": "current" } } ], "@babel/preset-react" ] } ================================================ FILE: .editorconfig ================================================ root = true [*] indent_style = space indent_size = 2 end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true ================================================ FILE: .eslintrc ================================================ { "extends": ["precure/auto"], "rules": { "@typescript-eslint/explicit-function-return-type": "off" } } ================================================ FILE: .gitattributes ================================================ * text=auto *.js text eol=lf ================================================ FILE: .github/ISSUE_TEMPLATE.md ================================================ - version: - `node` version: - `npm` (or `yarn`) version: **Do you want to request a *feature* or report a *bug*?:** **What is the current behavior?:** **What is the expected behavior?:** **Suggested solution:** ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ **What**: **Why**: **How**: **Checklist**: * [ ] Documentation * [ ] Tests * [ ] Ready to be merged ================================================ FILE: .github/workflows/test.yml ================================================ name: test on: [push] jobs: build: runs-on: ubuntu-latest strategy: matrix: # LTS (20.x) and latest (22.x) only node-version: [20.x, 22.x] steps: - uses: actions/checkout@v2 - name: Setup node uses: actions/setup-node@v1 with: node-version: ${{ matrix.node }} - run: npm install - run: npm run lint - run: npm run test ================================================ FILE: .gitignore ================================================ # Dependencies node_modules/ # Build outputs lib/ dist/ compiled/ # Cache files .eslintcache .test-cache # OS files .DS_Store Thumbs.db # IDE files .vscode/ .idea/ *.swp *.swo # Logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* # Runtime data pids *.pid *.seed *.pid.lock # Coverage directory used by tools like istanbul coverage/ .nyc_output/ ================================================ FILE: .husky/pre-commit ================================================ npx lint-staged ================================================ FILE: .prettierignore ================================================ **/test/fixtures/** .github dist package.json ================================================ FILE: .prettierrc ================================================ { "semi": false, "singleQuote": true, "trailingComma": "none" } ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: - Using welcoming and inclusive language - Being respectful of differing viewpoints and experiences - Gracefully accepting constructive criticism - Focusing on what is best for the community - Showing empathy towards other community members Examples of unacceptable behavior by participants include: - The use of sexualized language or imagery and unwelcome sexual attention or advances - Trolling, insulting/derogatory comments, and personal or political attacks - Public or private harassment - Publishing others' private information, such as a physical or electronic address, without explicit permission - Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at akameco.t@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/4/ ================================================ FILE: eslint.config.mjs ================================================ // ESLint v9+ config import js from '@eslint/js' import tseslint from 'typescript-eslint' export default [ js.configs.recommended, ...tseslint.configs.recommended, { files: ['src/**/*.ts'], languageOptions: { parser: tseslint.parser, parserOptions: { ecmaVersion: 2022, sourceType: 'module' } } }, { ignores: ['dist/', 'node_modules/', 'compiled/'] } ] ================================================ FILE: example/basic/babel.config.js ================================================ module.exports = function (api) { api.cache(true) return { presets: ['react-app'] } } ================================================ FILE: example/basic/i18n/.keep ================================================ ================================================ FILE: example/basic/i18n/en.json ================================================ { "App": { "hello": "Hello Button", "submit": "Submit Button" }, "a": { "hello": "hello", "world": "world" } } ================================================ FILE: example/basic/i18n/ja.json ================================================ { "App": { "hello": "", "submit": "" }, "a": { "hello": "", "world": "" } } ================================================ FILE: example/basic/package.json ================================================ { "name": "basic", "version": "1.0.0", "license": "MIT", "scripts": { "i18n": "NODE_ENV=development extract-messages -l=en,ja -o i18n -d en 'src/**/*.{js,jsx}'" }, "dependencies": { "react": "^16.13.1", "react-intl": "^4.3.1" }, "devDependencies": { "@babel/core": "^7.9.0", "babel-preset-react-app": "^9.1.2", "extract-react-intl-messages": "latest" } } ================================================ FILE: example/basic/src/App.jsx ================================================ import React from 'react' import { injectIntl, useIntl } from 'react-intl' export const SubmitButton = injectIntl(({ intl }) => { const label = intl.formatMessage({ id: 'App.submit', defaultMessage: 'Submit Button' }) return }) export const HelloButton = () => { const intl = useIntl() const label = intl.formatMessage({ id: 'App.hello', defaultMessage: 'Hello Button' }) return } ================================================ FILE: example/basic/src/messages.js ================================================ /* eslint-disable import/no-extraneous-dependencies */ import { defineMessages } from 'react-intl' export default defineMessages({ hello: { id: 'a.hello', defaultMessage: 'hello' }, world: { id: 'a.world', defaultMessage: 'world' } }) ================================================ FILE: example/with-typescript/babel.config.js ================================================ module.exports = function (api) { api.cache(true) return { presets: ['@babel/preset-react', '@babel/preset-typescript'] } } ================================================ FILE: example/with-typescript/i18n/en.json ================================================ { "App": { "hello": "Hello Button", "submit": "Submit Button" }, "a": { "hello": "hello", "world": "world" } } ================================================ FILE: example/with-typescript/i18n/ja.json ================================================ { "App": { "hello": "", "submit": "" }, "a": { "hello": "", "world": "" } } ================================================ FILE: example/with-typescript/package.json ================================================ { "name": "with-typescript", "version": "1.0.0", "license": "MIT", "scripts": { "i18n": "extract-messages -l=en,ja -o i18n -d en 'src/**/*.{ts,tsx}'" }, "dependencies": { "react": "^16.13.1", "react-intl": "^4.3.1" }, "devDependencies": { "@babel/core": "^7.9.0", "@babel/preset-react": "^7.9.4", "@babel/preset-typescript": "^7.9.0", "extract-react-intl-messages": "latest", "typescript": "^3.8.3" } } ================================================ FILE: example/with-typescript/src/App.tsx ================================================ import React from 'react' import { injectIntl, useIntl } from 'react-intl' export const SubmitButton = injectIntl(({ intl }) => { const label = intl.formatMessage({ id: 'App.submit', defaultMessage: 'Submit Button' }) return }) export const HelloButton = () => { const intl = useIntl() const label = intl.formatMessage({ id: 'App.hello', defaultMessage: 'Hello Button' }) return } ================================================ FILE: example/with-typescript/src/messages.ts ================================================ import { defineMessages } from 'react-intl' export default defineMessages({ hello: { id: 'a.hello', defaultMessage: 'hello' }, world: { id: 'a.world', defaultMessage: 'world' } }) ================================================ FILE: jest.config.js ================================================ export default { testPathIgnorePatterns: [ '[/\\\\](dist|compiled|node_modules)[/\\\\]' ], testEnvironment: 'node', preset: 'ts-jest', extensionsToTreatAsEsm: ['.ts'], moduleNameMapper: { '^(\\.{1,2}/.*)\\.js$': '$1' }, transform: { '^.+\\.ts$': [ 'ts-jest', { useESM: true, tsconfig: { module: 'esnext' } } ] }, transformIgnorePatterns: ['node_modules/(?!(.*\\.mjs$))'] } ================================================ FILE: license ================================================ The MIT License (MIT) Copyright (c) akameco (akameco.github.io) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: package.json ================================================ { "name": "extract-react-intl-messages", "version": "5.0.0", "description": "Extract react-intl messages", "license": "MIT", "repository": "akameco/extract-react-intl-messages", "type": "module", "author": { "name": "akameco", "email": "akameco.t@gmail.com", "url": "https://akameco.github.io" }, "engines": { "node": ">=20" }, "main": "dist/index.js", "scripts": { "fmt": "prettier --write .", "example": "./cli.js -l=en,ja -o example/i18n -d en 'example/**/*.{js,tsx}'", "example:yaml": "./cli.js -l=en,ja -f=yaml -o example/i18n -d en 'example/**/*.{js,tsx}'", "prepublish": "npm run build", "build": "tsc", "lint": "eslint src/**/*.ts --fix --cache", "pretest": "node -e \"import('fs/promises').then(fs => fs.rm('.test-cache', {recursive: true, force: true})).catch(() => {})\"", "test": "jest", "posttest": "node -e \"import('fs/promises').then(fs => fs.rm('.test-cache', {recursive: true, force: true})).catch(() => {})\"", "prepare": "husky" }, "bin": { "extract-messages": "dist/cli.js", "extract-react-intl-messages": "dist/cli.js" }, "files": [ "dist" ], "keywords": [ "react", "i18n", "intl", "react-intl", "extract", "json", "messages" ], "dependencies": { "@babel/core": "^7.28.3", "babel-plugin-react-intl": "^7.9.4", "deepmerge": "^4.3.1", "file-entry-cache": "^5.0.1", "flat": "^5.0.2", "glob": "11.0.3", "js-yaml": "4.1.0", "load-json-file": "^6.2.0", "lodash.merge": "^4.6.2", "meow": "13.2.0", "read-babelrc-up": "^1.1.0", "sort-keys": "^4.2.0", "write-json-file": "^4.3.0" }, "devDependencies": { "@babel/plugin-proposal-class-properties": "^7.18.6", "@babel/preset-env": "^7.28.3", "@babel/preset-flow": "^7.27.1", "@babel/preset-react": "^7.27.1", "@types/file-entry-cache": "^5.0.4", "@types/flat": "^5.0.5", "@types/js-yaml": "^3.12.10", "@types/load-json-file": "^2.0.7", "@types/lodash.merge": "^4.6.9", "@types/node": "^24.3.0", "@types/temp-write": "^4.0.0", "@types/write-json-file": "^2.2.1", "babel-plugin-react-intl-auto": "^3.3.0", "eslint": "^9.34.0", "husky": "9.1.7", "jest": "^30.1.1", "lint-staged": "16.1.5", "prettier": "3.6.2", "temp-write": "^4.0.0", "tempy": "^0.5.0", "ts-jest": "^29.4.1", "typescript": "^5.9.2", "typescript-eslint": "^8.41.0" }, "lint-staged": { "*.ts": [ "prettier --write", "eslint --fix" ], "*.{js,json,md}": [ "prettier --write" ] } } ================================================ FILE: readme.md ================================================ # extract-react-intl-messages [![test](https://github.com/akameco/extract-react-intl-messages/workflows/test/badge.svg)](https://github.com/akameco/extract-react-intl-messages/actions?query=workflow%3Atest) [![tested with jest](https://img.shields.io/badge/tested_with-jest-99424f.svg)](https://github.com/facebook/jest) [![styled with prettier](https://img.shields.io/badge/styled_with-prettier-ff69b4.svg)](https://github.com/prettier/prettier) [![MIT License](https://img.shields.io/npm/l/nps.svg?style=flat-square)](./license) [![All Contributors](https://img.shields.io/badge/all_contributors-14-orange.svg?style=flat-square)](#contributors-) This package will generate json or yaml files from a glob. It will generate one file per locale, with the ids of each message defined by the [`defineMessages`](https://github.com/yahoo/react-intl/wiki/API#definemessages) function of [react-intl](https://github.com/yahoo/react-intl). The value of each of these keys will be an empty string, except for your `defaultLocale` which will be populated with the [`defaultMessage`](https://github.com/yahoo/react-intl/wiki/API#message-descriptor). ## Dependencies ### Babel - 0.x works with Babel 6 ## Install ``` $ npm install --save-dev extract-react-intl-messages ``` ## Usage app/components/App/messages.js ```js import { defineMessages, useIntl } from 'react-intl' export default defineMessages({ hello: { id: 'a.hello', defaultMessage: 'hello' }, world: { id: 'a.world', defaultMessage: 'world' } }) export const SubmitButton = () => { const intl = useIntl() const label = intl.formatMessage({ id: 'a.submit', defaultMessage: 'Submit Button' }) return } ``` ### Run Script ``` $ extract-messages -l=en,ja -o app/translations -d en --flat false './app/**/!(*.test).js' ``` ### Output app/translations/en.json ```json { "a": { "hello": "hello", "world": "world", "submit": "Submit Button" } } ``` app/translations/ja.json ```json { "a": { "hello": "", "world": "", "submit": "" } } ``` ## Recommend Use with [babel-plugin-react-intl-auto: i18n for the component age. Auto management react-intl ID.](https://github.com/akameco/babel-plugin-react-intl-auto) ## CLI ```console $ extract-messages --help Extract react-intl messages Usage $ extract-react-intl-messages $ extract-messages Options -o, --output Output directory [require: true] -l, --locales locales [require: true] -f, --format json | yaml [default: json] -d, --defaultLocale default locale --overwriteDefault default: true --flat json [default: true] | yaml [default: false] --indent default: 2 Example $ extract-messages --locales=ja,en --output app/translations 'app/**/*.js' $ extract-messages -l=ja,en -o i18n 'src/**/*.js' $ extract-messages -l=ja,en -o app/translations -f yaml 'app/**/messages.js' ``` ### create-react-app user create `.babelrc` like this. ```json { "presets": ["react-app"] } ``` Run with `NODE_ENV=development`. ``` $ NODE_ENV=development extract-messages ... ``` ### TypeScript babel required. See [example/with-typescript](example/with-typescript) ``` npm install --save-dev @babel/core @babel/preset-typescript @babel/preset-react ``` `babel.config.js` ```js module.exports = function (api) { api.cache(true) return { presets: ['@babel/preset-react', '@babel/preset-typescript'] } } ``` ## API ### extractReactIntlMessages(locales, input, buildDir, [options]) #### locales Type: `Array` Example: `['en', 'ja']` #### input Type: `Array` Target files. glob. #### buildDir Type: `string` Export directory. #### options ##### defaultLocale Type: `string`
Default: `en` ##### format Type: `json` | `yaml`
Default: `json` Set extension to output. ##### overwriteDefault Type: `boolean`
Default: true If overwriteDefault is `false`, it will not overwrite messages in the default locale. ##### indent Type: `number`
Default: `2` ##### flat Type: `boolean`
Default: `true` If format is `yaml`, set to `false`. Be careful if `false`. See [this issue](https://github.com/akameco/extract-react-intl-messages/issues/3). ##### babel-plugin-react-intl's Options See https://github.com/formatjs/formatjs/tree/master/packages/babel-plugin-react-intl#options ## Contributors Thanks goes to these wonderful people ([emoji key](https://github.com/kentcdodds/all-contributors#emoji-key)):
akameco
akameco

💻 ⚠️ 📖 🚇
Hoan Tran
Hoan Tran

💻 ⚠️
giantpinkwalrus
giantpinkwalrus

💻
enrique-ramirez
enrique-ramirez

📖
Stefan Gojan
Stefan Gojan

🐛 💻 ⚠️
Solomon English
Solomon English

💻
Filip "Filson" Pasternak
Filip "Filson" Pasternak

💻
nodaguti
nodaguti

💻 ⚠️
fix-fix
fix-fix

💻
bradbarrow
bradbarrow

🐛 💻 ⚠️
Gregor MacLennan
Gregor MacLennan

💻
Dmitry Zarva
Dmitry Zarva

💻
Michael Pan
Michael Pan

💡
Tom Erik Støwer
Tom Erik Støwer

💻
Bart Lens
Bart Lens

💻
Truong Hoang Dung
Truong Hoang Dung

💡
Nestoro
Nestoro

💻
Yutaro Kido
Yutaro Kido

💻
This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind welcome! ## License MIT © [akameco](http://akameco.github.io) ================================================ FILE: src/cli.ts ================================================ #!/usr/bin/env node import meow from 'meow' import extractMessage from './index.js' const cli = meow( ` Usage $ extract-react-intl-messages $ extract-messages Options -o, --output Output directory [require: true] -l, --locales locales [require: true] -f, --format json | yaml [default: json] -d, --default-locale default locale --overwriteDefault [default: true] --flat json [default: true] | yaml [default: false] --cache [default: false] --cacheLocation [default: .extract-react-intl-messages-cache] --indent default: 2 Example $ extract-messages --locales=ja,en --output app/translations 'app/**/*.js' $ extract-messages -l=ja,en -o app/translations -f yaml 'app/**/messages.js' `, { importMeta: import.meta, flags: { flat: { type: 'boolean' }, output: { type: 'string', shortFlag: 'o' }, locales: { type: 'string', shortFlag: 'l' }, format: { type: 'string', shortFlag: 'f', default: 'json' }, defaultLocale: { type: 'string', shortFlag: 'd' }, overwriteDefault: { type: 'boolean', default: true }, withDescriptions: { type: 'boolean', default: false }, // babel-plugin-react-intl boolean options extractSourceLocation: { type: 'boolean', default: false }, removeDefaultMessage: { type: 'boolean' }, extractFromFormatMessageCall: { type: 'boolean', default: true }, indent: { type: 'number', default: 2 } } } ) const { output, locales } = cli.flags if (!output) { console.log('ERROR: required output') process.exit(1) } if (!locales || typeof locales !== 'string') { console.log('ERROR: required locales') process.exit(1) } const localesMap = locales.split(',') /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ extractMessage(localesMap, cli.input[0], output, cli.flags as any) ================================================ FILE: src/extract-react-intl/index.ts ================================================ import path from 'path' import { glob } from 'glob' import { promisify } from 'util' import merge from 'lodash.merge' import deepmerge from 'deepmerge' import { resolvePlugin, resolvePreset, transformFile, PluginItem } from '@babel/core' import readBabelrcUp from 'read-babelrc-up' import babelPluginReactIntl from 'babel-plugin-react-intl' import fileEntryCache, { FileDescriptor } from 'file-entry-cache' type LocaleMap = Record> const localeMap = (arr: string[]): LocaleMap => arr.reduce((obj: LocaleMap, x: string) => { obj[x] = {} return obj }, {}) const createResolveList = (fn: (name: string, dirname: string) => string | null) => (list: PluginItem[], cwd: string) => list.map((x) => (typeof x === 'string' ? fn(x, cwd) : x)) const resolvePresets = createResolveList(resolvePreset) const resolvePlugins = createResolveList(resolvePlugin) const getBabelrc = (cwd: string) => { try { const babelrc = readBabelrcUp.sync({ cwd }).babel if (!babelrc.env) { return babelrc } const env = process.env.BABEL_ENV || process.env.NODE_ENV || 'development' return deepmerge(babelrc, babelrc.env[env] || {}) } catch { return { presets: [], plugins: [] } } } const getBabelrcDir = (cwd: string) => path.dirname(readBabelrcUp.sync({ cwd }).path) const babelPluginReactIntlOptions = new Set([ 'moduleSourceName', 'extractSourceLocation', 'messagesDir', 'overrideIdFn', 'removeDefaultMessage', 'extractFromFormatMessageCall', 'additionalComponentNames' ]) type Options = { [key: string]: unknown defaultLocale?: string cwd?: string cache?: boolean cacheLocation?: string withDescriptions?: boolean } type Message = { id: string defaultMessage: string description: string } type CacheData = { defaultLocale: string localeMap: LocaleMap } type File = FileDescriptor & { meta: { data: CacheData } } export default async ( locales: string[], pattern: string, { defaultLocale = 'en', withDescriptions = false, cwd = process.cwd(), cache: cacheEnabled = false, cacheLocation = '.extract-react-intl-messages-cache', ...pluginOptions }: Options = {} ) => { if (!Array.isArray(locales)) { throw new TypeError(`Expected a Array, got ${typeof locales}`) } if (typeof pattern !== 'string') { throw new TypeError(`Expected a string, got ${typeof pattern}`) } const babelrc = getBabelrc(cwd) || {} const babelrcDir = getBabelrcDir(cwd) const presets = babelrc.presets || [] const plugins = babelrc.plugins || [] if ( !plugins.find( (plugin: PluginItem) => (Array.isArray(plugin) ? plugin[0] : plugin) === 'react-intl' ) ) { // Append a the `react-intl` babel plugin only when it isn’t already included in the babel config presets.unshift({ plugins: [ [ babelPluginReactIntl, Object.entries(pluginOptions).reduce((acc, [key, value]) => { if (babelPluginReactIntlOptions.has(key)) { return { ...acc, [key]: value } } return acc }, {}) ] ] }) } const files: string[] = await glob(pattern) if (files.length === 0) { throw new Error(`File not found (${pattern})`) } const cachePath = path.resolve(cacheLocation) const cacheDirname = path.dirname(cachePath) const cacheBasename = path.basename(cachePath) const cache = cacheEnabled ? fileEntryCache.create(cacheBasename, cacheDirname) : null const extractFromFile = async (file: string) => { const babelOpts = { presets: resolvePresets(presets, babelrcDir), plugins: resolvePlugins(plugins, babelrcDir) } /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ const transformResult = await promisify(transformFile as any)( file, babelOpts ) /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ const { metadata } = transformResult as { metadata: any } const localeObj = localeMap(locales) const result = metadata['react-intl'].messages as Message[] for (const { id, defaultMessage, description } of result) { for (const locale of locales) { const message = defaultLocale === locale ? defaultMessage : '' localeObj[locale][id] = withDescriptions ? { message, description } : message } } return localeObj } const extractFromCache = async (file: string) => { if (cache === null) { return extractFromFile(file) } const cachedLocaleObj = cache?.getFileDescriptor(file) as File | undefined const changed = cachedLocaleObj?.changed const data = cachedLocaleObj?.meta.data if (changed === false && data?.defaultLocale === defaultLocale) { return data.localeMap } const localeObj = await extractFromFile(file) if (cachedLocaleObj) { cachedLocaleObj.meta.data = { defaultLocale, localeMap: localeObj } } return localeObj } const extract = cacheEnabled ? extractFromCache : extractFromFile const arr = await Promise.all(files.map(extract)) if (cache) { cache.reconcile() } return arr.reduce((h, obj) => merge(h, obj), localeMap(locales)) } ================================================ FILE: src/extract-react-intl/readme.md ================================================ # extract-react-intl ## API ### extractReactIntl(locales, pattern, [options]) Return a `Promise` wrapped extracted messages. #### locales Type: `Array` Example: `['en', 'ja']` #### pattern Type: `string` File path with glob. #### options Additional options. #### defaultLocale Type: `string`
Default: `en` Set default locale for your app. ##### cwd Type: `string`
Default: `.` **You most likely don't need this.** Change run path. ================================================ FILE: src/extract-react-intl/test/__snapshots__/test.ts.snap ================================================ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing exports[`babel plugin execution order 1`] = ` { "en": { "src.extract-react-intl.test.pluginOrdering.test": "auto", }, } `; exports[`extract from file 1`] = ` { "en": { "components.App.1248161314": "Submit button", "components.App.hello": "hello", "components.App.world": "world", "components/Greeting/welcome": " Welcome {name}, you have received {unreadCount, plural, =0 {no new messages} one {{formattedUnreadCount} new message} other {{formattedUnreadCount} new messages} } since {formattedLastLoginTime}. ", }, "ja": { "components.App.1248161314": "", "components.App.hello": "", "components.App.world": "", "components/Greeting/welcome": "", }, } `; exports[`extract from file by enabling cache and extract from cache 1`] = ` { "en": { "components.App.1248161314": "Submit button", "components.App.hello": "hello", "components.App.world": "world", "components/Greeting/welcome": " Welcome {name}, you have received {unreadCount, plural, =0 {no new messages} one {{formattedUnreadCount} new message} other {{formattedUnreadCount} new messages} } since {formattedLastLoginTime}. ", }, "ja": { "components.App.1248161314": "", "components.App.hello": "", "components.App.world": "", "components/Greeting/welcome": "", }, } `; exports[`extract from file by enabling cache and extract from cache 2`] = ` { "en": { "components.App.1248161314": "Submit button", "components.App.hello": "hello", "components.App.world": "world", "components/Greeting/welcome": " Welcome {name}, you have received {unreadCount, plural, =0 {no new messages} one {{formattedUnreadCount} new message} other {{formattedUnreadCount} new messages} } since {formattedLastLoginTime}. ", }, "ja": { "components.App.1248161314": "", "components.App.hello": "", "components.App.world": "", "components/Greeting/welcome": "", }, } `; exports[`extract from file with descriptions 1`] = ` { "en": { "components.App.hello": { "description": "hello message description", "message": "hello", }, "components.App.world": { "description": "world message description", "message": "world", }, "components/Greeting/welcome": { "description": "Welcome message description", "message": " Welcome {name}, you have received {unreadCount, plural, =0 {no new messages} one {{formattedUnreadCount} new message} other {{formattedUnreadCount} new messages} } since {formattedLastLoginTime}. ", }, }, "ja": { "components.App.hello": { "description": "hello message description", "message": "", }, "components.App.world": { "description": "world message description", "message": "", }, "components/Greeting/welcome": { "description": "Welcome message description", "message": "", }, }, } `; ================================================ FILE: src/extract-react-intl/test/fixtures/.babelrc ================================================ { "presets": [ [ "@babel/preset-env", { "targets": { "browsers": [ "last 2 versions", "safari >= 7" ] } } ], "@babel/preset-react", "@babel/preset-flow" ], "plugins": [ "@babel/plugin-proposal-class-properties" ], "env": { "react-intl": { "plugins": [ [ "react-intl-auto", { "removePrefix": "src.extract-react-intl.test.fixtures" } ] ] } } } ================================================ FILE: src/extract-react-intl/test/fixtures/components/App/index.js ================================================ // @flow import React, { Component } from 'react' import { FormattedMessage, injectIntl } from 'react-intl' import Greeting from '../Greeting' import messages from './messages' injectIntl(({ intl }) => { const label = intl.formatMessage({ defaultMessage: "Submit button" }) return }); export default class App extends Component { render() { const user = { name: 'Eric', unreadCount: 4, lastLoginTime: Date.now() - 1000 * 60 * 60 * 24 } return (
) } } ================================================ FILE: src/extract-react-intl/test/fixtures/components/App/messages.js ================================================ // @flow import { defineMessages } from 'react-intl' export default defineMessages({ // hello message description hello: 'hello', // world message description world: 'world' }) ================================================ FILE: src/extract-react-intl/test/fixtures/components/Greeting/index.js ================================================ // @flow import React, { Component } from 'react' import { FormattedMessage, FormattedNumber, FormattedRelative } from 'react-intl' import messages from './messages' type Props = { user: { name: string, unreadCount: number, lastLoginTime: number } } export default class Greeting extends Component { props: Props render() { const { user } = this.props return (

{user.name}, unreadCount: user.unreadCount, formattedUnreadCount: ( ), formattedLastLoginTime: ( ) }} />

) } } function defaultMessage() { return 'hello' } ================================================ FILE: src/extract-react-intl/test/fixtures/components/Greeting/messages.js ================================================ // @flow import { defineMessages } from 'react-intl' export default defineMessages({ // Welcome message description welcome: { id: 'components/Greeting/welcome', defaultMessage: ` Welcome {name}, you have received {unreadCount, plural, =0 {no new messages} one {{formattedUnreadCount} new message} other {{formattedUnreadCount} new messages} } since {formattedLastLoginTime}. ` } }) ================================================ FILE: src/extract-react-intl/test/fixtures/components/LanguageProvider/index.js ================================================ // @flow import React, { Component } from 'react' import { addLocaleData, IntlProvider } from 'react-intl' import enLocaleData from 'react-intl/locale-data/en' import jaLocaleData from 'react-intl/locale-data/ja' import enMessages from '../../translations/en.json' import jaMessages from '../../translations/ja.json' addLocaleData(enLocaleData) addLocaleData(jaLocaleData) const messages = { en: enMessages, ja: jaMessages } export default class LanguageProvider extends Component { props: { children?: React$Element<*> } state: { locale: string } = { locale: 'en' } render() { const { locale } = this.state return ( ) } } ================================================ FILE: src/extract-react-intl/test/fixtures/index.html ================================================ Example
================================================ FILE: src/extract-react-intl/test/fixtures/index.js ================================================ // @flow import React from 'react' import ReactDOM from 'react-dom' import App from './components/App' import LanguageProvider from './components/LanguageProvider' ReactDOM.render( , document.getElementById('root') ) ================================================ FILE: src/extract-react-intl/test/pluginOrdering/.babelrc ================================================ { "presets": ["@babel/preset-flow"], "plugins": ["react-intl-auto"] } ================================================ FILE: src/extract-react-intl/test/pluginOrdering/messages.js ================================================ import { defineMessages } from 'react-intl' export default defineMessages({ test: 'auto' }) ================================================ FILE: src/extract-react-intl/test/resolution/.babelrc ================================================ { "presets": ["@babel/preset-flow"], "plugins": ["react-intl"] } ================================================ FILE: src/extract-react-intl/test/resolution/messages.js ================================================ import { defineMessages } from 'react-intl' export default defineMessages({ test: { id: 'test', defaultMessage: 'test' } }) ================================================ FILE: src/extract-react-intl/test/test.ts ================================================ import m from '..' const pattern = 'src/extract-react-intl/test/fixtures/**/*.js' const locales = ['en', 'ja'] test('extract from file', async () => { process.env.BABEL_ENV = 'react-intl' const x = await m(locales, pattern, { defaultLocale: 'en', cwd: `${__dirname}/fixtures`, extractFromFormatMessageCall: true }) expect(x).toMatchSnapshot() }) test('extract from file by enabling cache and extract from cache', async () => { process.env.BABEL_ENV = 'react-intl' const x = await m(locales, pattern, { defaultLocale: 'en', cwd: `${__dirname}/fixtures`, extractFromFormatMessageCall: true, cache: true, cacheLocation: '.test-cache' }) expect(x).toMatchSnapshot() const y = await m(locales, pattern, { defaultLocale: 'en', cwd: `${__dirname}/fixtures`, extractFromFormatMessageCall: true, cache: true, cacheLocation: '.test-cache' }) expect(y).toMatchSnapshot() }) // TODO: fix test.skip('babelrc path resolution', async () => { const x = await m(['en'], './extract-react-intl/test/resolution/**/*.js', { defaultLocale: 'en', cwd: `${__dirname}/resolution` }) expect(x).toMatchSnapshot() }) test('babel plugin execution order', async () => { const x = await m( ['en'], 'src/extract-react-intl/test/pluginOrdering/**/*.js', { defaultLocale: 'en', cwd: `${__dirname}/pluginOrdering` } ) expect(x).toMatchSnapshot() }) test('error', async () => { expect.assertions(1) await m(locales, 'notfound', { defaultLocale: 'en', cwd: `${__dirname}/fixtures` }).catch((error) => { expect(error.message).toMatch('File not found') }) }) test('extract from file with descriptions', async () => { process.env.BABEL_ENV = 'react-intl' const x = await m(locales, pattern, { defaultLocale: 'en', cwd: './test/fixtures', withDescriptions: true }) expect(x).toMatchSnapshot() }) ================================================ FILE: src/global.d.ts ================================================ declare module 'read-babelrc-up' { function sync(opts: { cwd: string }): { path: string babel: import('@babel/core').TransformOptions } } ================================================ FILE: src/index.ts ================================================ import path from 'path' import fs from 'fs' import yaml from 'js-yaml' import { promisify } from 'util' import { flatten, unflatten } from 'flat' import loadJsonFile from 'load-json-file' import writeJsonFile from 'write-json-file' import sortKeys from 'sort-keys' import _extractReactIntl from './extract-react-intl/index.js' const writeJson = (outputPath: string, obj: object, indent: number) => { return writeJsonFile(`${outputPath}.json`, obj, { indent }) } const writeYaml = (outputPath: string, obj: object, indent: number) => { return promisify(fs.writeFile)( `${outputPath}.yml`, yaml.dump(obj, { indent }), 'utf8' ) } const isJson = (ext: string) => ext === 'json' function loadLocaleFiles(locales: string[], buildDir: string, ext: string) { const oldLocaleMaps: Record> = {} try { fs.mkdirSync(buildDir, { recursive: true }) } catch { // Directory already exists or other error, continue } for (const locale of locales) { const file = path.resolve(buildDir, `${locale}.${ext}`) // Initialize json file try { const output = isJson(ext) ? JSON.stringify({}) : yaml.dump({}) fs.writeFileSync(file, output, { flag: 'wx' }) } catch (error: unknown) { if ((error as NodeJS.ErrnoException).code !== 'EEXIST') { throw error } } let messages = isJson(ext) ? loadJsonFile.sync(file) : yaml.load(fs.readFileSync(file, 'utf8'), { json: true }) messages = flatten(messages) oldLocaleMaps[locale] = {} for (const messageKey of Object.keys(messages as object)) { const message = (messages as Record)[messageKey] if (message && typeof message === 'string' && message !== '') { oldLocaleMaps[locale][messageKey] = ( messages as Record )[messageKey] as object } } } return oldLocaleMaps } type Opts = { [key: string]: unknown defaultLocale: string format?: string flat?: boolean overwriteDefault?: boolean indent?: number } const extractMessage = async ( locales: string[], pattern: string, buildDir: string, { format = 'json', flat = isJson(format), defaultLocale = 'en', overwriteDefault = true, indent = 2, ...opts }: Opts = { defaultLocale: 'en' } ) => { if (!Array.isArray(locales)) { throw new TypeError(`Expected a Array, got ${typeof locales}`) } if (typeof pattern !== 'string') { throw new TypeError(`Expected a string, got ${typeof pattern}`) } if (typeof buildDir !== 'string') { throw new TypeError(`Expected a string, got ${typeof buildDir}`) } const ext = isJson(format) ? 'json' : 'yml' const oldLocaleMaps = loadLocaleFiles(locales, buildDir, ext) const extractorOptions = { defaultLocale, withDescriptions: false, cwd: process.cwd(), extractFromFormatMessageCall: true, ...opts } const newLocaleMaps = await _extractReactIntl( locales, pattern, extractorOptions ) return Promise.all( locales.map((locale) => { // If the default locale, overwrite the origin file let localeMap = locale === defaultLocale && overwriteDefault ? // Create a clone so we can use only current valid messages below { ...oldLocaleMaps[locale], ...newLocaleMaps[locale] } : { ...newLocaleMaps[locale], ...oldLocaleMaps[locale] } // Only keep existing keys localeMap = Object.fromEntries( Object.entries(localeMap).filter(([key]) => Object.keys(newLocaleMaps[locale]).includes(key) ) ) const fomattedLocaleMap: object = flat ? sortKeys(localeMap, { deep: true }) : sortKeys(unflatten(localeMap, { object: true }), { deep: true }) const fn = isJson(format) ? writeJson : writeYaml return fn(path.resolve(buildDir, locale), fomattedLocaleMap, indent) }) ) } extractMessage.extractReactIntl = _extractReactIntl export default extractMessage ================================================ FILE: src/test/fixtures/custom/a/messages.js ================================================ import { defineMessages } from '../i18n' export default defineMessages({ hello: { id: 'a.custom.hello', defaultMessage: 'hello' }, world: { id: 'a.custom.world', defaultMessage: 'world' } }) ================================================ FILE: src/test/fixtures/custom/b/messages.js ================================================ import { defineMessages } from '../i18n' export default defineMessages({ hello: { id: 'b.custom.message', defaultMessage: 'Message' } }) ================================================ FILE: src/test/fixtures/custom/i18n.js ================================================ export { defineMessages } from 'react-intl' ================================================ FILE: src/test/fixtures/default/a/App.js ================================================ import React from 'react' import { useIntl } from 'react-intl' export const SubmitButton = () => { const intl = useIntl() const label = intl.formatMessage({ id: 'a.submit', defaultMessage: 'Submit Button' }) return } ================================================ FILE: src/test/fixtures/default/a/messages.js ================================================ import { defineMessages } from 'react-intl' export default defineMessages({ hello: { id: 'a.hello', defaultMessage: 'hello' }, world: { id: 'a.world', defaultMessage: 'world' } }) ================================================ FILE: src/test/fixtures/default/b/messages.js ================================================ import { defineMessages } from 'react-intl' export default defineMessages({ hello: { id: 'b.hello', defaultMessage: 'hello' }, world: { id: 'b.world', defaultMessage: 'world' } }) ================================================ FILE: src/test/fixtures/removed/a/messages.js ================================================ import { defineMessages } from 'react-intl' export default defineMessages({ hello: { id: 'a.hello', defaultMessage: 'hello' } }) ================================================ FILE: src/test/fixtures/removed/b/messages.js ================================================ import { defineMessages } from 'react-intl' export default defineMessages({ hello: { id: 'b.hello', defaultMessage: 'hello' }, world: { id: 'b.world', defaultMessage: 'world' } }) ================================================ FILE: src/test/fixtures/unsorted/a/messages.js ================================================ import { defineMessages } from 'react-intl' export default defineMessages({ hello: { id: 'a.hello', defaultMessage: 'hello' }, helloZ: { id: 'z.hello', defaultMessage: 'hello' }, helloC: { id: 'c.hello', defaultMessage: 'hello' }, world: { id: 'a.world', defaultMessage: 'world' } }) ================================================ FILE: src/test/fixtures/unsorted/b/messages.js ================================================ import { defineMessages } from 'react-intl' export default defineMessages({ hello: { id: 'b.hello', defaultMessage: 'hello' }, helloY: { id: 'y.hello', defaultMessage: 'hello' }, world: { id: 'b.world', defaultMessage: 'world' } }) ================================================ FILE: src/test/json/__snapshots__/test.ts.snap ================================================ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing exports[`export json - nest 1`] = ` { "a": { "hello": "hello", "submit": "Submit Button", "world": "world", }, "b": { "hello": "hello", "world": "world", }, } `; exports[`export json - nest 2`] = ` { "a": { "hello": "", "submit": "", "world": "", }, "b": { "hello": "", "world": "", }, } `; exports[`export json 1`] = ` { "a.hello": "hello", "a.submit": "Submit Button", "a.world": "world", "b.hello": "hello", "b.world": "world", } `; exports[`export json 2`] = ` { "a.hello": "", "a.submit": "", "a.world": "", "b.hello": "", "b.world": "", } `; exports[`export json with removed messages 1`] = ` { "a.hello": "hello", "a.submit": "Submit Button", "a.world": "world", "b.hello": "hello", "b.world": "world", } `; exports[`export json with removed messages 2`] = ` { "a.hello": "", "a.submit": "", "a.world": "", "b.hello": "", "b.world": "", } `; exports[`export json with removed messages 3`] = ` { "a.hello": "hello", "b.hello": "hello", "b.world": "world", } `; exports[`export json with removed messages 4`] = ` { "a.hello": "", "b.hello": "", "b.world": "", } `; exports[`export using custom module 1`] = ` { "a.custom.hello": "hello", "a.custom.world": "world", "b.custom.message": "Message", } `; exports[`export using custom module 2`] = ` { "a.custom.hello": "", "a.custom.world": "", "b.custom.message": "", } `; exports[`sort keys 1`] = ` [ "a.hello", "a.world", "b.hello", "b.world", "c.hello", "y.hello", "z.hello", ] `; exports[`sort keys 2`] = ` [ "a.hello", "a.world", "b.hello", "b.world", "c.hello", "y.hello", "z.hello", ] `; exports[`with overwriteDefault 1`] = ` { "a.custom.hello": "hello", "a.custom.world": "world", "b.custom.message": "Default Message", } `; exports[`with overwriteDefault 2`] = ` { "a.custom.hello": "", "a.custom.world": "", "b.custom.message": "", } `; ================================================ FILE: src/test/json/test.ts ================================================ import fs from 'fs' import path from 'path' import tempy from 'tempy' import m from '../..' test('export json', async () => { const tmp = tempy.directory() await m(['en', 'ja'], 'src/test/fixtures/default/**/*.js', tmp) const en = JSON.parse(fs.readFileSync(path.resolve(tmp, 'en.json'), 'utf8')) const ja = JSON.parse(fs.readFileSync(path.resolve(tmp, 'ja.json'), 'utf8')) expect(en).toMatchSnapshot() expect(ja).toMatchSnapshot() }) test('export json with removed messages', async () => { const tmp = tempy.directory() await m(['en', 'ja'], 'src/test/fixtures/default/**/*.js', tmp) const enBefore = JSON.parse( fs.readFileSync(path.resolve(tmp, 'en.json'), 'utf8') ) const jaBefore = JSON.parse( fs.readFileSync(path.resolve(tmp, 'ja.json'), 'utf8') ) expect(enBefore).toMatchSnapshot() expect(jaBefore).toMatchSnapshot() await m(['en', 'ja'], 'src/test/fixtures/removed/**/*.js', tmp) const en = JSON.parse(fs.readFileSync(path.resolve(tmp, 'en.json'), 'utf8')) const ja = JSON.parse(fs.readFileSync(path.resolve(tmp, 'ja.json'), 'utf8')) expect(en).toMatchSnapshot() expect(ja).toMatchSnapshot() }) test('export json - nest', async () => { const tmp = tempy.directory() await m(['en', 'ja'], 'src/test/fixtures/default/**/*.js', tmp, { defaultLocale: 'en', flat: false }) const en = JSON.parse(fs.readFileSync(path.resolve(tmp, 'en.json'), 'utf8')) const ja = JSON.parse(fs.readFileSync(path.resolve(tmp, 'ja.json'), 'utf8')) expect(en).toMatchSnapshot() expect(ja).toMatchSnapshot() }) test('sort keys', async () => { const tmp = tempy.directory() const enPath = path.resolve(tmp, 'en.json') const jaPath = path.resolve(tmp, 'ja.json') await m(['en', 'ja'], 'src/test/fixtures/unsorted/**/*.js', tmp) const en = JSON.parse(fs.readFileSync(enPath, 'utf8')) const ja = JSON.parse(fs.readFileSync(jaPath, 'utf8')) expect(Object.keys(en)).toMatchSnapshot() expect(Object.keys(ja)).toMatchSnapshot() }) test('export using custom module', async () => { const tmp = tempy.directory() await m(['en', 'ja'], 'src/test/fixtures/custom/**/*.js', tmp, { defaultLocale: 'en', moduleSourceName: '../i18n' }) const en = JSON.parse(fs.readFileSync(path.resolve(tmp, 'en.json'), 'utf8')) const ja = JSON.parse(fs.readFileSync(path.resolve(tmp, 'ja.json'), 'utf8')) expect(en).toMatchSnapshot() expect(ja).toMatchSnapshot() }) test('with overwriteDefault', async () => { const tmp = tempy.directory() fs.writeFileSync( path.resolve(tmp, 'en.json'), JSON.stringify( { 'a.custom.hello': 'hello', 'a.custom.world': 'world', 'b.custom.message': 'Default Message' }, null, 2 ), 'utf8' ) await m(['en', 'ja'], 'src/test/fixtures/custom/**/*.js', tmp, { defaultLocale: 'en', moduleSourceName: '../i18n', overwriteDefault: false }) const en = JSON.parse(fs.readFileSync(path.resolve(tmp, 'en.json'), 'utf8')) const ja = JSON.parse(fs.readFileSync(path.resolve(tmp, 'ja.json'), 'utf8')) expect(en).toMatchSnapshot() expect(ja).toMatchSnapshot() }) ================================================ FILE: src/test/test.ts ================================================ import m from '..' test('errors', async () => { // @ts-expect-error testing invalid argument type await expect(m('hello')).rejects.toThrow('Expected a Array') // @ts-expect-error testing invalid argument type await expect(m(['en', 'ja'], 2)).rejects.toThrow('Expected a string') // @ts-expect-error testing invalid argument type await expect(m(['en', 'ja'], 'app/', 2)).rejects.toThrow('Expected a string') }) ================================================ FILE: src/test/yaml/__snapshots__/test.ts.snap ================================================ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing exports[`export yaml - flat 1`] = ` { "a.hello": "hello", "a.submit": "Submit Button", "a.world": "world", "b.hello": "hello", "b.world": "world", } `; exports[`export yaml - flat 2`] = ` { "a.hello": "", "a.submit": "", "a.world": "", "b.hello": "", "b.world": "", } `; exports[`export yaml 1`] = ` { "a": { "hello": "hello", "submit": "Submit Button", "world": "world", }, "b": { "hello": "hello", "world": "world", }, } `; exports[`export yaml 2`] = ` { "a": { "hello": "", "submit": "", "world": "", }, "b": { "hello": "", "world": "", }, } `; exports[`exsit yaml 1`] = ` { "a": { "hello": "hello", "submit": "Submit Button", "world": "world", }, "b": { "hello": "hello", "world": "world", }, } `; exports[`exsit yaml 2`] = ` { "a": { "hello": "hello2", "submit": "", "world": "", }, "b": { "hello": "", "world": "", }, } `; ================================================ FILE: src/test/yaml/test.ts ================================================ import fs from 'fs' import path from 'path' import tempy from 'tempy' import tempWrite from 'temp-write' import yaml from 'js-yaml' import m from '../..' const fixturesPath = 'src/test/fixtures/default/**/*.js' const yamlLoad = (tmp: string, file = '') => yaml.load(fs.readFileSync(path.resolve(tmp, file), 'utf8')) const defaultLocale = 'en' test('export yaml', async () => { const tmp = tempy.directory() await m(['en', 'ja'], fixturesPath, tmp, { defaultLocale, format: 'yaml' }) expect(yamlLoad(tmp, 'en.yml')).toMatchSnapshot() expect(yamlLoad(tmp, 'ja.yml')).toMatchSnapshot() }) test('export yaml - flat', async () => { const tmp = tempy.directory() await m(['en', 'ja'], fixturesPath, tmp, { defaultLocale, format: 'yaml', flat: true }) expect(yamlLoad(tmp, 'en.yml')).toMatchSnapshot() expect(yamlLoad(tmp, 'ja.yml')).toMatchSnapshot() }) test('exsit yaml', async () => { const x = { a: { hello: 'hello2' } } const tmpEn = tempWrite.sync(yaml.dump(x), 'en.yml') await m(['en'], fixturesPath, path.dirname(tmpEn), { defaultLocale, format: 'yaml' }) expect(yaml.load(fs.readFileSync(tmpEn, 'utf8'))).toMatchSnapshot() const tmpJa = tempWrite.sync(yaml.dump(x), 'ja.yml') await m(['ja'], fixturesPath, path.dirname(tmpJa), { defaultLocale, format: 'yaml' }) expect(yaml.load(fs.readFileSync(tmpJa, 'utf8'))).toMatchSnapshot() }) ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { /* Language and Environment */ "target": "ES2022", "lib": ["ES2022", "DOM"], "module": "Node16", "moduleResolution": "Node16", /* Modules */ "allowImportingTsExtensions": false, "resolveJsonModule": true, "allowSyntheticDefaultImports": true, "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "isolatedModules": true, /* Emit */ "declaration": true, "declarationMap": true, "sourceMap": true, "outDir": "dist", "removeComments": false, "importHelpers": false, "downlevelIteration": false, /* Interop Constraints */ "allowJs": false, "checkJs": false, /* Type Checking */ "strict": true, "noImplicitAny": true, "strictNullChecks": true, "strictFunctionTypes": true, "strictBindCallApply": true, "strictPropertyInitialization": true, "noImplicitThis": true, "useUnknownInCatchVariables": true, "alwaysStrict": true, "noUnusedLocals": true, "noUnusedParameters": true, "exactOptionalPropertyTypes": false, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "noUncheckedIndexedAccess": false, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": false, /* Completeness */ "skipLibCheck": true }, "include": ["src/**/*"], "exclude": [ "node_modules", "dist", "**/*.test.ts", "**/*.spec.ts", "**/test.*" ] }