├── typing.d.ts ├── .nvmrc ├── packages ├── docs │ ├── static │ │ ├── .nojekyll │ │ └── img │ │ │ └── airport-logo.png │ ├── babel.config.js │ ├── docs │ │ ├── LS-Manager │ │ │ ├── img │ │ │ │ ├── pat.png │ │ │ │ ├── login.png │ │ │ │ ├── language.png │ │ │ │ ├── locales.png │ │ │ │ └── gitForWindow.png │ │ │ ├── How to use │ │ │ │ ├── img │ │ │ │ │ ├── excel.png │ │ │ │ │ ├── after_pr.png │ │ │ │ │ ├── change.png │ │ │ │ │ ├── commit.png │ │ │ │ │ ├── editing.png │ │ │ │ │ ├── fileio.png │ │ │ │ │ ├── finding.png │ │ │ │ │ ├── refresh.png │ │ │ │ │ └── refresh_icon.png │ │ │ │ ├── _category_.json │ │ │ │ ├── 02-Creating Pull Requests.md │ │ │ │ └── 01-Editing translations.md │ │ │ ├── _category_.json │ │ │ ├── Installation.md │ │ │ ├── Introduction.md │ │ │ ├── Prerequisites.md │ │ │ └── Project Setup.md │ │ ├── Airport │ │ │ ├── img │ │ │ │ └── lsKeyDisplay.png │ │ │ ├── _category_.json │ │ │ ├── types.md │ │ │ ├── installation.md │ │ │ ├── introduction.md │ │ │ └── API.md │ │ ├── Introduction copy.md │ │ └── Introduction.md │ ├── src │ │ ├── components │ │ │ └── HomepageFeatures │ │ │ │ ├── program.png │ │ │ │ ├── split.png │ │ │ │ ├── conversion.png │ │ │ │ ├── styles.module.css │ │ │ │ └── index.tsx │ │ ├── pages │ │ │ ├── markdown-page.md │ │ │ ├── index.module.css │ │ │ └── index.tsx │ │ └── css │ │ │ └── custom.css │ ├── tsconfig.json │ ├── .gitignore │ ├── README.md │ ├── sidebars.ts │ ├── package.json │ └── docusaurus.config.ts ├── tsconfig.json ├── js │ ├── src │ │ ├── index.ts │ │ ├── __tests__ │ │ │ ├── Airport-performance.test.ts │ │ │ ├── Airport-translation.test.ts │ │ │ ├── Airport.test.ts │ │ │ └── Airport-currency.test.ts │ │ ├── utils.ts │ │ ├── types.ts │ │ └── Airport.ts │ ├── tsconfig.json │ ├── jest.config.js │ ├── package.json │ └── rollup.config.mjs ├── react │ ├── src │ │ ├── index.ts │ │ ├── __tests__ │ │ │ ├── test-utils.tsx │ │ │ └── useAirport-translation.test.tsx │ │ ├── AirportSubtree.tsx │ │ ├── AirportProvider.tsx │ │ └── useAirport.tsx │ ├── tsconfig.json │ ├── jest.config.js │ ├── rollup.config.mjs │ └── package.json ├── tsconfig.settings.json └── clean-ls-loader │ ├── package.json │ ├── README.md │ └── index.js ├── images ├── t-example.png ├── airport-logo.png └── subtree-diagram.png ├── lerna.json ├── .gitignore ├── README.md ├── .prettierrc ├── tsconfig.json ├── LICENSE.md ├── package.json └── NOTICE /typing.d.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 18.20.2 2 | -------------------------------------------------------------------------------- /packages/docs/static/.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/t-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naver/airport/main/images/t-example.png -------------------------------------------------------------------------------- /images/airport-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naver/airport/main/images/airport-logo.png -------------------------------------------------------------------------------- /images/subtree-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naver/airport/main/images/subtree-diagram.png -------------------------------------------------------------------------------- /packages/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [{ "path": "js" }, { "path": "react" }] 4 | } 5 | -------------------------------------------------------------------------------- /packages/js/src/index.ts: -------------------------------------------------------------------------------- 1 | export { Airport } from './Airport' 2 | 3 | export * from './types' 4 | export * from './utils' 5 | -------------------------------------------------------------------------------- /packages/docs/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')], 3 | }; 4 | -------------------------------------------------------------------------------- /packages/docs/docs/LS-Manager/img/pat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naver/airport/main/packages/docs/docs/LS-Manager/img/pat.png -------------------------------------------------------------------------------- /packages/docs/static/img/airport-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naver/airport/main/packages/docs/static/img/airport-logo.png -------------------------------------------------------------------------------- /packages/docs/docs/LS-Manager/img/login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naver/airport/main/packages/docs/docs/LS-Manager/img/login.png -------------------------------------------------------------------------------- /packages/docs/docs/Airport/img/lsKeyDisplay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naver/airport/main/packages/docs/docs/Airport/img/lsKeyDisplay.png -------------------------------------------------------------------------------- /packages/docs/docs/LS-Manager/img/language.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naver/airport/main/packages/docs/docs/LS-Manager/img/language.png -------------------------------------------------------------------------------- /packages/docs/docs/LS-Manager/img/locales.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naver/airport/main/packages/docs/docs/LS-Manager/img/locales.png -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/*" 4 | ], 5 | "npmClient": "yarn", 6 | "useWorkspaces": true, 7 | "version": "1.0.0" 8 | } 9 | -------------------------------------------------------------------------------- /packages/docs/docs/LS-Manager/img/gitForWindow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naver/airport/main/packages/docs/docs/LS-Manager/img/gitForWindow.png -------------------------------------------------------------------------------- /packages/docs/docs/LS-Manager/How to use/img/excel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naver/airport/main/packages/docs/docs/LS-Manager/How to use/img/excel.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | yarn-error.log 2 | node_modules 3 | .cache 4 | .coverage 5 | .DS_Store 6 | .vscode 7 | *.tgz 8 | **/build/** 9 | *tsbuildinfo 10 | .rollup.cache -------------------------------------------------------------------------------- /packages/docs/docs/Airport/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Airport", 3 | "position": 2, 4 | "link": { 5 | "type": "generated-index" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/docs/docs/LS-Manager/How to use/img/after_pr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naver/airport/main/packages/docs/docs/LS-Manager/How to use/img/after_pr.png -------------------------------------------------------------------------------- /packages/docs/docs/LS-Manager/How to use/img/change.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naver/airport/main/packages/docs/docs/LS-Manager/How to use/img/change.png -------------------------------------------------------------------------------- /packages/docs/docs/LS-Manager/How to use/img/commit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naver/airport/main/packages/docs/docs/LS-Manager/How to use/img/commit.png -------------------------------------------------------------------------------- /packages/docs/docs/LS-Manager/How to use/img/editing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naver/airport/main/packages/docs/docs/LS-Manager/How to use/img/editing.png -------------------------------------------------------------------------------- /packages/docs/docs/LS-Manager/How to use/img/fileio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naver/airport/main/packages/docs/docs/LS-Manager/How to use/img/fileio.png -------------------------------------------------------------------------------- /packages/docs/docs/LS-Manager/How to use/img/finding.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naver/airport/main/packages/docs/docs/LS-Manager/How to use/img/finding.png -------------------------------------------------------------------------------- /packages/docs/docs/LS-Manager/How to use/img/refresh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naver/airport/main/packages/docs/docs/LS-Manager/How to use/img/refresh.png -------------------------------------------------------------------------------- /packages/docs/src/components/HomepageFeatures/program.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naver/airport/main/packages/docs/src/components/HomepageFeatures/program.png -------------------------------------------------------------------------------- /packages/docs/src/components/HomepageFeatures/split.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naver/airport/main/packages/docs/src/components/HomepageFeatures/split.png -------------------------------------------------------------------------------- /packages/docs/docs/LS-Manager/How to use/img/refresh_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naver/airport/main/packages/docs/docs/LS-Manager/How to use/img/refresh_icon.png -------------------------------------------------------------------------------- /packages/docs/src/components/HomepageFeatures/conversion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naver/airport/main/packages/docs/src/components/HomepageFeatures/conversion.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Airport Logo 3 |

4 | 5 | Check [official guide](https://naver.github.io/airport/) -------------------------------------------------------------------------------- /packages/docs/src/pages/markdown-page.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Markdown page example 3 | --- 4 | 5 | # Markdown page example 6 | 7 | You don't need React to write simple standalone pages. 8 | -------------------------------------------------------------------------------- /packages/docs/docs/Introduction copy.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 5 3 | --- 4 | # Changelog 5 | 6 | ### 2.0.0 7 | - Datetime related features have been deprecated. 8 | 9 | ### 1.0.0 10 | - Initial release. -------------------------------------------------------------------------------- /packages/docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // This file is not used in compilation. It is here just for a nice editor experience. 3 | "extends": "@docusaurus/tsconfig", 4 | "compilerOptions": { 5 | "baseUrl": "." 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/docs/docs/LS-Manager/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "LS-Manager", 3 | "position": 4, 4 | "link": { 5 | "type": "generated-index", 6 | "description": "Github based Language Set Manager For Non-developer" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/react/src/index.ts: -------------------------------------------------------------------------------- 1 | export { AirportProvider, AirportContext } from './AirportProvider' 2 | export { AirportSubtree } from './AirportSubtree' 3 | 4 | export { useAirport } from './useAirport' 5 | 6 | export * from 'airport-js' 7 | -------------------------------------------------------------------------------- /packages/docs/docs/LS-Manager/How to use/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "How to use", 3 | "position": 5, 4 | "link": { 5 | "type": "generated-index", 6 | "description": "Github based Language Set Manager For Non-developer" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/docs/src/components/HomepageFeatures/styles.module.css: -------------------------------------------------------------------------------- 1 | .features { 2 | display: flex; 3 | align-items: center; 4 | padding: 2rem 0; 5 | width: 100%; 6 | } 7 | 8 | .featureSvg { 9 | height: 200px; 10 | width: 200px; 11 | } 12 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": false, 6 | "singleQuote": true, 7 | "jsxSingleQuote": false, 8 | "trailingComma": "all", 9 | "bracketSpacing": true, 10 | "jsxBracketSameLine": true, 11 | "arrowParens": "avoid" 12 | } 13 | -------------------------------------------------------------------------------- /packages/docs/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /packages/tsconfig.settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "module": "esnext", 5 | "target": "es5", 6 | "moduleResolution": "node", 7 | "composite": true, 8 | "declaration": true, 9 | "declarationMap": true, 10 | "sourceMap": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/docs/docs/LS-Manager/Installation.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | --- 4 | 5 | `LS Manager` must be used with compatible `airport` version. 6 | Check your `airport` version and install matching `LS Manager` version. 7 | 8 | 9 | ### Airport `v1.0.0` ~ 10 | - Windows: [Download](https://booking.pstatic.net/airport/LS-Manager_win_v1.zip) 11 | - Mac: [Download](https://booking.pstatic.net/airport/LS-Manager_mac_v1.zip) -------------------------------------------------------------------------------- /packages/js/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.settings.json", 3 | "compilerOptions": { 4 | "outDir": "build", 5 | "rootDir": "src", 6 | "typeRoots": ["../../node_modules/@types", "./src"], 7 | "declaration": true, 8 | "declarationMap": true, 9 | "declarationDir": "build/dts", 10 | "emitDeclarationOnly": true, 11 | }, 12 | "include": [ 13 | "src/**/*" 14 | ], 15 | "exclude": [ 16 | "build", 17 | "**/__tests__" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es5", 5 | "lib": ["esnext", "dom"], 6 | "noImplicitAny": true, 7 | "declaration": true, 8 | "sourceMap": true, 9 | "experimentalDecorators": true, 10 | "emitDecoratorMetadata": true, 11 | "types": ["react", "jest", "node"], 12 | "typeRoots": ["node_modules/@types"] 13 | }, 14 | "exclude": ["node_modules", "__tests__", "packages/docs"] 15 | } 16 | -------------------------------------------------------------------------------- /packages/docs/src/pages/index.module.css: -------------------------------------------------------------------------------- 1 | /** 2 | * CSS files with the .module.css suffix will be treated as CSS modules 3 | * and scoped locally. 4 | */ 5 | 6 | .heroBanner { 7 | padding: 4rem 0; 8 | text-align: center; 9 | position: relative; 10 | overflow: hidden; 11 | } 12 | 13 | @media screen and (max-width: 996px) { 14 | .heroBanner { 15 | padding: 2rem; 16 | } 17 | } 18 | 19 | .buttons { 20 | display: flex; 21 | align-items: center; 22 | justify-content: center; 23 | } 24 | -------------------------------------------------------------------------------- /packages/react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.settings.json", 3 | "compilerOptions": { 4 | "outDir": "build", 5 | "rootDir": "src", 6 | "jsx": "react", 7 | "declaration": true, 8 | "declarationMap": true, 9 | "declarationDir": "build/dts", 10 | "emitDeclarationOnly": true, 11 | }, 12 | "references": [ 13 | { "path": "../js" }, 14 | ], 15 | "include": [ 16 | "src/**/*" 17 | ], 18 | "exclude": [ 19 | "build", 20 | "**/__tests__" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /packages/docs/docs/LS-Manager/Introduction.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | `LS Manager` is a GUI translation management tool that enables users with programming backgrounds to edit translations in the codebase. 6 | 7 | Using `LS Manager`, user is able to edit and create pull request in github for developers to confirm and merge modifications into the codebase. 8 | 9 | `LS Manager` also supports bulk modifications through external programs such as Microsoft Excel, with export and import features. 10 | 11 | `LS Manager` supports **MacOS** and **Windows**. -------------------------------------------------------------------------------- /packages/js/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | cacheDirectory: '.cache/jest', 3 | roots: ['/src'], 4 | transform: {'^.+\\.tsx?$': 'ts-jest'}, 5 | testMatch: ['**/__tests__/(*.)test.ts?(x)'], 6 | moduleFileExtensions: [ 7 | 'ts', 8 | 'tsx', 9 | 'js', 10 | 'json' 11 | ], 12 | coverageDirectory: '.coverage', 13 | collectCoverageFrom: [ 14 | 'src/**/*.ts?(x)', 15 | '!**/__tests__/(*.)test.ts?(x)', 16 | ], 17 | coverageReporters: [ 18 | 'html', 19 | 'clover' 20 | ], 21 | testURL: 'http://localhost', 22 | } 23 | -------------------------------------------------------------------------------- /packages/react/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | cacheDirectory: '.cache/jest', 3 | roots: ['/src'], 4 | transform: {'^.+\\.tsx?$': 'ts-jest'}, 5 | testMatch: ['**/__tests__/(*.)test.ts?(x)'], 6 | moduleFileExtensions: [ 7 | 'ts', 8 | 'tsx', 9 | 'js', 10 | 'json' 11 | ], 12 | coverageDirectory: '.coverage', 13 | collectCoverageFrom: [ 14 | 'src/**/*.ts?(x)', 15 | '!**/__tests__/(*.)test.ts?(x)', 16 | ], 17 | coverageReporters: [ 18 | 'html', 19 | 'clover' 20 | ], 21 | testURL: 'http://localhost', 22 | } 23 | -------------------------------------------------------------------------------- /packages/docs/docs/LS-Manager/Prerequisites.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 3 3 | --- 4 | ### Personal access token 5 | 6 | 1. Go to [Github(Personal access tokens (classic))](https://github.com/settings/tokens) and click on `Generate new token`. 7 | 8 | 2. Enter a name and expiration then check **repo**, **admin:org(read:org)**, **user**, **admin:enterprise(read:enterprise)** and generate the token. 9 | 10 | ![image](./img/pat.png) 11 | 12 | 3. Copy and save generated token somewhere to use to log in. 13 | 14 | ### Git for Windows (Windows) 15 | 16 | For Windows, you need to install [Git for Windows](https://gitforwindows.org) 17 | 18 | ![image](./img/gitForWindow.png) 19 | -------------------------------------------------------------------------------- /packages/docs/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | This website is built using [Docusaurus](https://docusaurus.io/), a modern static website generator. 4 | 5 | ### Installation 6 | 7 | ``` 8 | $ yarn 9 | ``` 10 | 11 | ### Local Development 12 | 13 | ``` 14 | $ yarn start 15 | ``` 16 | 17 | This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. 18 | 19 | ### Build 20 | 21 | ``` 22 | $ yarn build 23 | ``` 24 | 25 | This command generates static content into the `build` directory and can be served using any static contents hosting service. 26 | 27 | ### Deployment 28 | 29 | Deploy to github pages: 30 | ``` 31 | GIT_USER={Github username} GIT_PASS={Github token} yarn deploy 32 | ``` 33 | -------------------------------------------------------------------------------- /packages/docs/docs/LS-Manager/Project Setup.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 4 3 | --- 4 | ### 1. Login 5 | 6 | Enter your GitHub account email and [PAT token](/docs/LS-Manager/Prerequisites). 7 | 8 |
9 | ![login](./img/login.png) 10 | 11 |
12 | 13 | ### 2. Repository Configuration 14 | 15 | Enter git repository url, working branch, and airport configuration value (`supportedLocales`).
16 | It is recommended to match the order of inputting Supported Languages with the project. 17 | 18 | > Working branch refers to the branch that will be the base branch to be checked out from. 19 | 20 |
21 | ![local](./img/locales.png) 22 |
23 |
24 | ![language](./img/language.png) 25 |
26 | -------------------------------------------------------------------------------- /packages/docs/docs/LS-Manager/How to use/02-Creating Pull Requests.md: -------------------------------------------------------------------------------- 1 | Click on the Pull Request icon on the left and fill in the form. 2 | 3 |
4 | ![commit](./img/commit.png) 5 |
6 | 7 | After filling the form, click the `Submit` button to create a Pull Request. (If you have not merged previous PR, new changes will be pushed to the same branch) 8 | 9 | > We recommend to integrate CI to detect possible errors 10 | 11 |
12 | ![after_pr](./img/after_pr.png) 13 |
14 | 15 | Once the Pull Request has been merged, you can click on the left refresh icon to fetch the latest version of the target branch. 16 | 17 |
18 | ![refresh_icon](./img/refresh_icon.png) 19 | >Refresh(Reset) button fetches lastest LS of target branch 20 | 21 | ![refresh](./img/refresh.png) 22 | 23 |
24 | -------------------------------------------------------------------------------- /packages/clean-ls-loader/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "airport-clean-ls-loader", 3 | "version": "1.0.3", 4 | "author": "junsoo.choi@navercorp.com", 5 | "description": "webpack loader to select LS langagues to include in build phase.", 6 | "license": "MIT", 7 | "main": "index.js", 8 | "module": "index.js", 9 | "files": [ 10 | "index.js" 11 | ], 12 | "peerDependencies": { 13 | "typescript": "^5.0.0", 14 | "webpack": "^4.0.0 || ^5.0.0" 15 | }, 16 | "devDependencies": { 17 | "typescript": "^5.7.3", 18 | "webpack": "^5.97.1" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/naver/airport.git" 23 | }, 24 | "keywords": [ 25 | "internationalization", 26 | "i18n" 27 | ], 28 | "bugs": { 29 | "url": "https://github.com/naver/airport/issues" 30 | }, 31 | "homepage": "https://naver.github.io/airport" 32 | } 33 | -------------------------------------------------------------------------------- /packages/react/src/__tests__/test-utils.tsx: -------------------------------------------------------------------------------- 1 | // Airport 2 | // Copyright 2024-present NAVER Corp. 3 | // MIT License 4 | 5 | import * as React from 'react' 6 | import { render, RenderOptions } from '@testing-library/react' 7 | import { Airport } from 'airport-js' 8 | 9 | import { AirportProvider } from '../AirportProvider' 10 | 11 | const supportedLocales = ['ko', 'en'] as const 12 | type LocaleType = typeof supportedLocales[number] 13 | 14 | export const customRender = (ui: React.ReactElement, locale?: LocaleType, options?: Omit) => { 15 | const airport = new Airport({ 16 | supportedLocales, 17 | locale, 18 | fallbackLocale: 'ko', 19 | }) 20 | 21 | const DefaultProvider = ({ children }: { children: React.ReactNode }) => ( 22 | {children} 23 | ) 24 | 25 | return render(ui, { wrapper: DefaultProvider, ...options }) 26 | } 27 | -------------------------------------------------------------------------------- /packages/docs/sidebars.ts: -------------------------------------------------------------------------------- 1 | import type {SidebarsConfig} from '@docusaurus/plugin-content-docs'; 2 | 3 | /** 4 | * Creating a sidebar enables you to: 5 | - create an ordered group of docs 6 | - render a sidebar for each doc of that group 7 | - provide next/previous navigation 8 | 9 | The sidebars can be generated from the filesystem, or explicitly defined here. 10 | 11 | Create as many sidebars as you want. 12 | */ 13 | const sidebars: SidebarsConfig = { 14 | // By default, Docusaurus generates a sidebar from the docs folder structure 15 | tutorialSidebar: [{type: 'autogenerated', dirName: '.'}], 16 | 17 | // But you can create a sidebar manually 18 | /* 19 | tutorialSidebar: [ 20 | 'intro', 21 | 'hello', 22 | { 23 | type: 'category', 24 | label: 'Tutorial', 25 | items: ['tutorial-basics/create-a-document'], 26 | }, 27 | ], 28 | */ 29 | }; 30 | 31 | export default sidebars; 32 | -------------------------------------------------------------------------------- /packages/docs/docs/Introduction.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | # Introduction 5 | 6 | ## What is Airport? 7 | Airport is a comprehensive internalization library made to support text translations, number, datetime, currency conversions based on the locale you set. 8 | 9 | ## Why Airport? 10 | ### Comprehensive 11 | Airport supports most of the basic functions you need to service multiple regions. 12 | 13 | ### Translation code splitting 14 | Airport only imports translations that are required in the current page resulting in smaller bundle size. 15 | 16 | ### GUI translation management tool 17 | Airport comes with `LS Manager` which is a GUI translation management tool to edit translations in your source code. 18 | 19 | Because `LS Manager` creates pull requests with modifications, all developer has to do is final confirmation and merge into the codebase. `LS Manager` also supports bulk modifications through file export. 20 | -------------------------------------------------------------------------------- /packages/js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "airport-js", 3 | "version": "2.2.2", 4 | "author": "jeongseok oh , junsoo choi ", 5 | "license": "MIT", 6 | "types": "./build/index.d.ts", 7 | "main": "./build/index.cjs.js", 8 | "module": "./build/index.esm.js", 9 | "files": [ 10 | "build/**/*", 11 | "build/index.d.ts" 12 | ], 13 | "scripts": { 14 | "clean": "rimraf build .cache tsconfig.tsbuildinfo", 15 | "test": "jest", 16 | "build": "yarn clean && npx rollup -c" 17 | }, 18 | "dependencies": {}, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/naver/airport.git" 22 | }, 23 | "keywords": [ 24 | "internationalization", 25 | "i18n" 26 | ], 27 | "bugs": { 28 | "url": "https://github.com/naver/airport/issues" 29 | }, 30 | "homepage": "https://naver.github.io/airport", 31 | "description": "Comprehensive internationalization library" 32 | } 33 | -------------------------------------------------------------------------------- /packages/docs/docs/LS-Manager/How to use/01-Editing translations.md: -------------------------------------------------------------------------------- 1 | ### Single Editing 2 | 3 |
4 | ![finding](./img/finding.png) 5 |
6 | 7 | Find the LS you want to modify, click the pen button at the top right to edit, and apply modifications by clicking on the check mark. 8 | 9 |
10 | ![editing](./img/editing.png) 11 |
12 | 13 | You can check modified items by clicking on the LS id in `Local Changes` in the left partition. 14 | 15 |
16 | ![change](./img/change.png) 17 |
18 | 19 | ### Bulk editing 20 | 21 | Click on the Excel file icon in the sidebar to select mode and select `EXPORT`. 22 | 23 |
24 | ![fileio](./img/fileio.png) 25 |
26 | 27 | You can export LS of the current project in xlsx or json file format by clicking the `EXPORT` button. 28 | 29 |
30 | ![change](./img/excel.png) 31 |
32 | > Exported xlsx file 33 | 34 | Make desired changes using applications of your favor(ex: Microsoft Excel) and import edited file by clicking the same Excel file icon and `IMPORT` 35 | -------------------------------------------------------------------------------- /packages/docs/src/css/custom.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Any CSS included here will be global. The classic template 3 | * bundles Infima by default. Infima is a CSS framework designed to 4 | * work well for content-centric websites. 5 | */ 6 | 7 | /* You can override the default Infima variables here. */ 8 | :root { 9 | --ifm-color-primary: #16d6e7; 10 | --ifm-color-primary-dark: #16d6e7; 11 | --ifm-color-primary-darker: #16d6e7; 12 | --ifm-color-primary-darkest: #16d6e7; 13 | --ifm-color-primary-light: #16d6e7; 14 | --ifm-color-primary-lighter: #16d6e7; 15 | --ifm-color-primary-lightest: #16d6e7; 16 | --ifm-code-font-size: 95%; 17 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1); 18 | } 19 | 20 | /* For readability concerns, you should choose a lighter palette in dark mode. */ 21 | [data-theme='dark'] { 22 | --ifm-color-primary: #16d6e7; 23 | --ifm-color-primary-dark: #16d6e7; 24 | --ifm-color-primary-darker: #16d6e7; 25 | --ifm-color-primary-darkest: #16d6e7; 26 | --ifm-color-primary-light: #16d6e7; 27 | --ifm-color-primary-lighter: #16d6e7; 28 | --ifm-color-primary-lightest: #16d6e7; 29 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); 30 | } 31 | -------------------------------------------------------------------------------- /packages/docs/docs/Airport/types.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 4 3 | --- 4 | 5 | # Types 6 | 7 | > Type `LocaleText` in the guide is equivalent to `T[number]`(`string`) in the source code. 8 | 9 | ### Options 10 | ```tsx 11 | interface Options, G extends LS = {}> { 12 | supportedLocales: T 13 | locale: T[number] 14 | fallbackLocale: T[number] 15 | name?: string 16 | globalLS?: G 17 | currency?: LocaleMap 18 | currencyFormatValueKey?: string 19 | currencyFormat?: CurrencyMap 20 | keyCurrency?: CurrencyType 21 | exchangeRate?: CurrencyMap 22 | timezone?: TimezoneType 23 | timezoneData?: TimezoneDataMap 24 | localTimezoneOnly?: boolean 25 | } 26 | ``` 27 | 28 | ### ImprovedNumberFormatOptions 29 | ```tsx 30 | interface ImprovedNumberFormatOptions extends Intl.NumberFormatOptions { 31 | roundingMode?: RoundingMode // 'ceil' | 'floor' | 'round' 32 | } 33 | ``` 34 | 35 | ### LocaleMap 36 | ```tsx 37 | type LocaleMap, V> = { 38 | [locale in T[number]]: V 39 | } 40 | ``` 41 | 42 | ### CurrencyMap 43 | ```ts 44 | type CurrencyMap = { 45 | [currency in CurrencyType]?: T 46 | } 47 | ``` 48 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Airport
2 | Copyright 2024-present NAVER Corp.
3 | MIT License
4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "airport", 3 | "devDependencies": { 4 | "@rollup/plugin-commonjs": "^26.0.1", 5 | "@rollup/plugin-node-resolve": "^15.2.3", 6 | "@rollup/plugin-typescript": "^11.1.6", 7 | "@testing-library/dom": "^10.4.0", 8 | "@testing-library/jest-dom": "^6.5.0", 9 | "@testing-library/react": "^16.0.1", 10 | "@types/jest": "^26.0.14", 11 | "@types/node": "^14.14.6", 12 | "@types/react": "^18.2.43", 13 | "@types/react-dom": "^18.2.17", 14 | "jest": "^26.5.3", 15 | "lerna": "^3.22.1", 16 | "prettier": "^2.1.2", 17 | "rollup": "^4.21.1", 18 | "rollup-plugin-delete": "^2.0.0", 19 | "rollup-plugin-dts": "^6.1.1", 20 | "rollup-plugin-peer-deps-external": "^2.2.4", 21 | "ts-jest": "^26.4.1", 22 | "ts-loader": "^5.3.3", 23 | "typescript": "^4.0.3" 24 | }, 25 | "workspaces": [ 26 | "packages/*" 27 | ], 28 | "scripts": { 29 | "build": "lerna run build --scope airport*", 30 | "watch": "tsc -b -w packages", 31 | "clean": "lerna run --scope airport* --parallel clean", 32 | "clean:tsbuildinfo": "find . -name '*.tsbuildinfo' -type f -delete", 33 | "test": "lerna exec --scope airport* -- yarn test" 34 | }, 35 | "private": true 36 | } 37 | -------------------------------------------------------------------------------- /packages/js/rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import ts from "@rollup/plugin-typescript"; 2 | import resolve from "@rollup/plugin-node-resolve"; 3 | import commonjs from "@rollup/plugin-commonjs"; 4 | import dts from 'rollup-plugin-dts' 5 | import del from 'rollup-plugin-delete' 6 | import PeerDepsExternalPlugin from 'rollup-plugin-peer-deps-external'; 7 | 8 | export default [ 9 | { 10 | input: 'src/index.ts', 11 | output: [ 12 | { 13 | file: 'build/index.esm.js', 14 | format: 'es', 15 | }, 16 | { 17 | file: 'build/index.cjs.js', 18 | format: 'cjs', 19 | interop: 'esModule', 20 | } 21 | ], 22 | external: ['react'], 23 | plugins: [ 24 | PeerDepsExternalPlugin(), 25 | resolve({ extensions: ['.js', '.ts']}), 26 | commonjs(), 27 | ts({ tsconfig: './tsconfig.json'}), 28 | ], 29 | }, 30 | { 31 | input: "./build/dts/index.d.ts", 32 | output: [{ file: "build/index.d.ts", format: "es" }], 33 | plugins: [ 34 | dts(), 35 | del({ hook: 'buildEnd', targets: 'build/dts'}) 36 | ], 37 | }, 38 | ] -------------------------------------------------------------------------------- /packages/react/rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import ts from "@rollup/plugin-typescript"; 2 | import resolve from "@rollup/plugin-node-resolve"; 3 | import commonjs from "@rollup/plugin-commonjs"; 4 | import dts from 'rollup-plugin-dts' 5 | import del from 'rollup-plugin-delete' 6 | import PeerDepsExternalPlugin from 'rollup-plugin-peer-deps-external'; 7 | 8 | export default [ 9 | { 10 | input: 'src/index.ts', 11 | output: [ 12 | { 13 | file: 'build/index.esm.js', 14 | format: 'es', 15 | }, 16 | { 17 | file: 'build/index.cjs.js', 18 | format: 'cjs', 19 | interop: 'esModule', 20 | } 21 | ], 22 | external: ['airport-js', 'react'], 23 | plugins: [ 24 | PeerDepsExternalPlugin(), 25 | resolve({ extensions: ['.js', '.ts', '.tsx']}), 26 | commonjs(), 27 | ts({ tsconfig: './tsconfig.json'}), 28 | ], 29 | }, 30 | { 31 | input: "./build/dts/index.d.ts", 32 | output: [{ file: "build/index.d.ts", format: "es" }], 33 | plugins: [ 34 | dts(), 35 | del({ hook: 'buildEnd', targets: 'build/dts'}) 36 | ], 37 | }, 38 | ] -------------------------------------------------------------------------------- /packages/react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "airport-react", 3 | "version": "2.2.2", 4 | "author": "jeongseok oh , junsoo choi ", 5 | "license": "MIT", 6 | "types": "./build/index.d.ts", 7 | "main": "./build/index.cjs.js", 8 | "module": "./build/index.esm.js", 9 | "files": [ 10 | "build/**/*", 11 | "build/index.d.ts" 12 | ], 13 | "exports": { 14 | "require": "./build/index.cjs.js", 15 | "import": "./build/index.esm.js" 16 | }, 17 | "scripts": { 18 | "clean": "rimraf build .cache tsconfig.tsbuildinfo", 19 | "test": "jest", 20 | "build": "yarn clean && npx rollup -c" 21 | }, 22 | "dependencies": { 23 | "airport-js": "^2.2.2" 24 | }, 25 | "peerDependencies": { 26 | "react": "^16.8.0 || ^17.0.0 || ^18.0.0", 27 | "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" 28 | }, 29 | "repository": { 30 | "type": "git", 31 | "url": "git+https://github.com/naver/airport.git" 32 | }, 33 | "keywords": [ 34 | "internationalization", 35 | "i18n" 36 | ], 37 | "bugs": { 38 | "url": "https://github.com/naver/airport/issues" 39 | }, 40 | "homepage": "https://naver.github.io/airport", 41 | "description": "Comprehensive internationalization library" 42 | } 43 | -------------------------------------------------------------------------------- /packages/docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "start": "docusaurus start", 8 | "build": "docusaurus build", 9 | "swizzle": "docusaurus swizzle", 10 | "deploy": "docusaurus deploy", 11 | "clear": "docusaurus clear", 12 | "serve": "docusaurus serve", 13 | "write-translations": "docusaurus write-translations", 14 | "write-heading-ids": "docusaurus write-heading-ids", 15 | "typecheck": "tsc" 16 | }, 17 | "dependencies": { 18 | "@docusaurus/core": "3.1.1", 19 | "@docusaurus/preset-classic": "3.1.1", 20 | "@mdx-js/react": "^3.0.0", 21 | "clsx": "^2.0.0", 22 | "prism-react-renderer": "^2.3.0", 23 | "react": "^18.0.0", 24 | "react-dom": "^18.0.0" 25 | }, 26 | "devDependencies": { 27 | "@docusaurus/module-type-aliases": "3.1.1", 28 | "@docusaurus/tsconfig": "3.1.1", 29 | "@docusaurus/types": "3.1.1", 30 | "typescript": "~5.2.2" 31 | }, 32 | "browserslist": { 33 | "production": [ 34 | ">0.5%", 35 | "not dead", 36 | "not op_mini all" 37 | ], 38 | "development": [ 39 | "last 3 chrome version", 40 | "last 3 firefox version", 41 | "last 5 safari version" 42 | ] 43 | }, 44 | "engines": { 45 | "node": ">=18.0" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/docs/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import Link from '@docusaurus/Link'; 3 | import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; 4 | import Layout from '@theme/Layout'; 5 | import HomepageFeatures from '@site/src/components/HomepageFeatures'; 6 | import Heading from '@theme/Heading'; 7 | 8 | import styles from './index.module.css'; 9 | 10 | function HomepageHeader() { 11 | const {siteConfig} = useDocusaurusContext(); 12 | return ( 13 |
14 |
15 | 16 | {siteConfig.title} 17 | 18 |

{siteConfig.tagline}

19 |
20 | 23 | Start reading 24 | 25 |
26 |
27 |
28 | ); 29 | } 30 | 31 | export default function Home(): JSX.Element { 32 | const {siteConfig} = useDocusaurusContext(); 33 | return ( 34 | 37 | 38 |
39 | 40 |
41 |
42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /packages/docs/src/components/HomepageFeatures/index.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import Heading from '@theme/Heading'; 3 | import styles from './styles.module.css'; 4 | 5 | type FeatureItem = { 6 | title: string; 7 | Img: any 8 | description: JSX.Element; 9 | }; 10 | 11 | 12 | const FeatureList: FeatureItem[] = [ 13 | { 14 | title: 'Code splitting of language set', 15 | Img: require("./split.png").default, 16 | description: (<>), 17 | }, 18 | { 19 | title: 'Number / DateTime / Currency conversion', 20 | Img: require("./conversion.png").default, 21 | description: (<>), 22 | }, 23 | { 24 | title: 'GUI Translation management tool', 25 | Img: require("./program.png").default, 26 | description: (<>), 27 | }, 28 | ]; 29 | 30 | function Feature({title, Img, description}: FeatureItem) { 31 | return ( 32 |
33 |
34 | 35 |
36 |
37 | {title} 38 |

{description}

39 |
40 |
41 | ); 42 | } 43 | 44 | export default function HomepageFeatures(): JSX.Element { 45 | return ( 46 |
47 |
48 |
49 | {FeatureList.map((props, idx) => ( 50 | 51 | ))} 52 |
53 |
54 |
55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /packages/react/src/AirportSubtree.tsx: -------------------------------------------------------------------------------- 1 | // Airport 2 | // Copyright 2024-present NAVER Corp. 3 | // MIT License 4 | 5 | import { Airport } from 'airport-js' 6 | import * as React from 'react' 7 | 8 | import { AirportContext } from './AirportProvider' 9 | import { useAirport } from './useAirport' 10 | 11 | export interface Props { 12 | locale: string 13 | children: React.ReactNode 14 | name?: string 15 | } 16 | 17 | /** 18 | * **Airport context provider wrapper to apply separate locale from the global AirportProvider** 19 | */ 20 | export function AirportSubtree({ locale, name, children }: Props) { 21 | const [_, forceUpdate] = React.useReducer(x => x + 1, () => 0) 22 | const { initialOptions, } = useAirport() 23 | const localOptions = React.useMemo(() => { 24 | const options = { 25 | ...initialOptions, 26 | name, 27 | locale: locale, 28 | } 29 | 30 | return Object.freeze(options) 31 | }, [initialOptions]) 32 | const subtreeAirportInstance = React.useMemo(() => { 33 | return new Airport(localOptions) 34 | }, [localOptions]) 35 | const setLocale = React.useCallback((locale: string) => { 36 | subtreeAirportInstance.changeLocale(locale) 37 | forceUpdate() 38 | }, [subtreeAirportInstance]) 39 | 40 | React.useEffect(() => { 41 | if (!locale) return 42 | if (subtreeAirportInstance.getLocale() !== locale) { 43 | setLocale(locale) 44 | } 45 | }, [locale]) 46 | 47 | return 52 | {children} 53 | 54 | } 55 | -------------------------------------------------------------------------------- /packages/clean-ls-loader/README.md: -------------------------------------------------------------------------------- 1 | This is a package to use with `airport-js` or `airport-react` to remove unused languages in LS. 2 | 3 | - https://www.npmjs.com/package/airport-js 4 | - https://www.npmjs.com/package/airport-react 5 | 6 | ## installation 7 | 8 | - Make sure you have `typescript` and `webpack` installed in your project 9 | 10 | ``` 11 | npm install -D airport-clean-ls-loader typescript webpack 12 | yarn add -D airport-clean-ls-loader typescript webpack 13 | ``` 14 | 15 | ## Parameters 16 | 17 | - `process.env.AIRPORT_CLEAN_LS_FN_NAME`: function name to check LS. Usually the name of the function created using `createLSFactory()`. default is `createLS` 18 | - `process.env.AIRPORT_CLEAN_LS_LANGS_TO_REMOVE`: insert language codes to delete combined with `,` 19 | 20 | ## Example Usage: 21 | 22 | ``` 23 | AIRPORT_CLEAN_LS_LANGS_TO_REMOVE=ko,en yarn build 24 | ``` 25 | 26 | ```ts 27 | // next.config.js 28 | 29 | const moduleExports = { 30 | webpack: (config, { isServer, defaultLoaders, webpack }) => { 31 | config.module.rules.push({ 32 | test: /\.tsx?$/, 33 | use: [{ loader: 'airport-clean-ls-loader' }], 34 | }) 35 | 36 | return config 37 | }, 38 | } 39 | ``` 40 | 41 | 62 | -------------------------------------------------------------------------------- /packages/js/src/__tests__/Airport-performance.test.ts: -------------------------------------------------------------------------------- 1 | // Airport 2 | // Copyright 2024-present NAVER Corp. 3 | // MIT License 4 | 5 | import { Airport } from '../Airport' 6 | import { Currency } from '../types' 7 | 8 | describe('Airport performance test', () => { 9 | const supportedLocales = ['ko-KR', 'en-US', 'ja-JP'] as const 10 | 11 | let airport: Airport 12 | beforeEach(() => { 13 | airport = new Airport({ 14 | supportedLocales, 15 | locale: 'ko-KR', 16 | fallbackLocale: 'ko-KR', 17 | currency: { 18 | 'ko-KR': Currency.KRW, 19 | 'en-US': Currency.USD, 20 | 'ja-JP': Currency.JPY, 21 | }, 22 | }) 23 | }) 24 | 25 | test('Intl.NumberFormat performance test', () => { 26 | console.time('Intl.NumberFormat performance test') 27 | 28 | for (let i = 0; i < 100000; i++) { 29 | new Intl.NumberFormat('ko-KR').format(10000) 30 | } 31 | 32 | console.timeEnd('Intl.NumberFormat performance test') 33 | }) 34 | 35 | test('fn function performance test', () => { 36 | console.time('fn performance test') 37 | 38 | for (let i = 0; i < 100000; i++) { 39 | airport.fn(10000) 40 | } 41 | 42 | console.timeEnd('fn performance test') 43 | }) 44 | 45 | test('fn function performance test when the roundingMode option is given', () => { 46 | console.time('roundingMode fn performance test') 47 | 48 | for (let i = 0; i < 100000; i++) { 49 | airport.fn(10000, { roundingMode: 'ceil' }) 50 | } 51 | 52 | console.timeEnd('roundingMode fn performance test') 53 | }) 54 | 55 | test(`fc function performace test`, () => { 56 | console.time('fc performance test') 57 | 58 | for (let i = 0; i < 100000; i++) { 59 | airport.fc(10000) 60 | } 61 | 62 | console.timeEnd('fc performance test') 63 | }) 64 | }) 65 | -------------------------------------------------------------------------------- /packages/docs/docs/Airport/installation.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | --- 4 | 5 | # Installation 6 | 7 | ## Use airport with vanillaJS 8 | 9 | ### 1. Installation 10 | Using npm: 11 | ```bash 12 | npm install airport-js 13 | ``` 14 | Using yarn: 15 | ```bash 16 | yarn add airport-js 17 | ``` 18 | 19 | ### 2. Instantiate airport 20 | ```ts 21 | import { Airport, createLSFactory } from 'airport-js'; 22 | 23 | const airport = new Airport({ 24 | supportedLocales:['ko', 'en'], 25 | locale:"ko", 26 | fallbackLocale:"ko", 27 | currency :{ 28 | ko: 'KRW', 29 | en: 'USD', 30 | }, 31 | currencyFormat:{ 32 | KRW: '{price}원', 33 | USD: '${price}', 34 | }, 35 | currencyFormatValueKey:'price', 36 | exchangeRate:{ 37 | USD: 1, 38 | KRW: 1000, 39 | }, 40 | }) 41 | 42 | const createLS = createLSFactory() 43 | 44 | const LLS = createLS({ 45 | hello: { 46 | ko: '안녕하세요', 47 | en: 'Hello' 48 | } 49 | }) 50 | 51 | console.log(airport.t(LLS.hello)) 52 | ``` 53 | 54 | 55 | 56 | ## Use airport with ReactJS 57 | 58 | ### 1. Installation 59 | Using npm: 60 | ```bash 61 | npm install airport-react 62 | ``` 63 | 64 | Using yarn: 65 | ```bash 66 | yarn add airport-react 67 | ``` 68 | 69 | ### 2. Add Provider to Root Container 70 | ```tsx 71 | import { AirportProvider } from 'airport-react' 72 | import App from './App' 73 | 74 | // Declare supportedLocales as const for Typescript typing 75 | const supportedLocales = ['ko', 'en'] as const 76 | 77 | function Root() { 78 | return 83 | 84 | 85 | } 86 | ``` 87 | 88 | ### 3. use Airport through `useAirport` 89 | ```tsx 90 | import * as React from 'react' 91 | import { useAirport } from 'airport-react' 92 | 93 | function App() { 94 | const { setLocale, t, fc } = useAirport() 95 | // ... 96 | } 97 | ``` 98 | -------------------------------------------------------------------------------- /packages/js/src/utils.ts: -------------------------------------------------------------------------------- 1 | // Airport 2 | // Copyright 2024-present NAVER Corp. 3 | // MIT License 4 | 5 | import { LS, PartialLS, RoundingMode } from './types' 6 | 7 | /** 8 | * **createLS wrapper function** 9 | * 10 | * @typeParam `T` - Array of supported locales 11 | * @typeParam `isAllRequired` - Type that determines whether partialLS is supported 12 | * 13 | * @example 14 | * const supportLocales = ['ko', 'en'] as const 15 | * const createLS = createLSFactory() 16 | */ 17 | export function createLSFactory, isAllRequired extends Boolean = true>() { 18 | return function createLS : PartialLS>(ls: L): L { 19 | return ls 20 | } 21 | } 22 | 23 | const offsetRegEx = new RegExp(/[+-]\d\d:?\d\d|Z/) 24 | 25 | // Deep compare function to determine whether NumberFormat instance has an option change 26 | // Caution - this function is not considering all the common deep comparison cases of javascript 27 | export function deepEqual(obj1: any, obj2: any) { 28 | if (Number.isNaN(obj1) && Number.isNaN(obj2)) { 29 | return true 30 | } 31 | 32 | if (typeof obj1 !== typeof obj2) { 33 | return false 34 | } 35 | 36 | if (typeof obj1 !== 'object' || typeof obj2 != 'object' || obj1 === null || obj2 === null) { 37 | return obj1 === obj2 38 | } 39 | 40 | const keys1 = Object.keys(obj1) 41 | const keys2 = Object.keys(obj2) 42 | if (keys1.length !== keys2.length) { 43 | return false 44 | } 45 | 46 | for (let key of keys1) { 47 | if (!deepEqual(obj1[key], obj2[key])) { 48 | return false 49 | } 50 | } 51 | 52 | return true 53 | } 54 | 55 | // Revaluating functions to support the 'roundingMode' option 56 | export function roundOperation(originNumber: number, adjustedNumber: number, roundingMode: RoundingMode): number { 57 | const reAdjustmentDiffNumber = 10 ** (Math.floor(Math.log10(Math.abs(originNumber - adjustedNumber))) + 1) 58 | 59 | if (roundingMode === 'ceil' && adjustedNumber <= originNumber) { 60 | return adjustedNumber + reAdjustmentDiffNumber 61 | } 62 | if (roundingMode === 'floor' && adjustedNumber >= originNumber) { 63 | return adjustedNumber - reAdjustmentDiffNumber 64 | } 65 | return adjustedNumber 66 | } 67 | -------------------------------------------------------------------------------- /packages/react/src/AirportProvider.tsx: -------------------------------------------------------------------------------- 1 | // Airport 2 | // Copyright 2024-present NAVER Corp. 3 | // MIT License 4 | 5 | import * as React from 'react' 6 | 7 | import { Airport, LS, Options, PartialLS } from 'airport-js' 8 | 9 | export interface AirportContextType, G extends LS | PartialLS> { 10 | airport: Airport 11 | initialOptions: Options 12 | setLocale: (locale: L[number]) => void 13 | subtreeLocale?: L[number] 14 | setSubtreeLocale?: (nextStrictLocale: L[number]) => void 15 | } 16 | 17 | export const AirportContext = (, G extends LS | PartialLS = {}>() => 18 | React.createContext>(null))() 19 | 20 | export interface Props, G extends LS = {}> extends Partial> { 21 | children: React.ReactNode 22 | airport?: Airport 23 | } 24 | 25 | /** 26 | * **Airport context provider** 27 | * 28 | * **Airport provider needs an airport instance to be used in the context** 29 | */ 30 | export function AirportProvider, G extends LS | PartialLS = {}>({ 31 | children, 32 | airport, 33 | ...props 34 | }: Props) { 35 | const airportInstance = React.useMemo(() => { 36 | if (airport) { 37 | return airport 38 | } 39 | 40 | return new Airport(props as Options) 41 | }, [airport]) 42 | const initialOptions = React.useMemo(() => { 43 | if (airport) { 44 | return Object.freeze(airport.getOptions()) 45 | } 46 | 47 | return Object.freeze(props as Options) 48 | }, [airport]) 49 | const [_, forceUpdate] = React.useReducer( 50 | x => x + 1, 51 | () => 0, 52 | ) 53 | const setLocale = React.useCallback( 54 | (locale: L[number]) => { 55 | airportInstance.changeLocale(locale) 56 | forceUpdate() 57 | }, 58 | [airportInstance], 59 | ) 60 | 61 | React.useEffect(() => { 62 | if (!props.locale) return 63 | if (airportInstance.getLocale() !== props.locale) { 64 | setLocale(props.locale) 65 | } 66 | }, [airportInstance, props.locale]) 67 | 68 | return ( 69 | 70 | {children} 71 | 72 | ) 73 | } 74 | -------------------------------------------------------------------------------- /packages/docs/docusaurus.config.ts: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------ 2 | // This file was auto-created by docusaurus-cli 3 | // ------------------------------------------------ 4 | 5 | import {themes as prismThemes} from 'prism-react-renderer'; 6 | import type {Config} from '@docusaurus/types'; 7 | import type * as Preset from '@docusaurus/preset-classic'; 8 | 9 | const config: Config = { 10 | title: 'Airport', 11 | tagline: 'Comprehensive internationalization library', 12 | favicon: 'img/airport-logo.png', 13 | 14 | // Set the production url of your site here 15 | url: 'https://naver.github.io', 16 | // Set the // pathname under which your site is served 17 | // For GitHub pages deployment, it is often '//' 18 | baseUrl: '/airport', 19 | 20 | // GitHub pages deployment config. 21 | // If you aren't using GitHub pages, you don't need these. 22 | organizationName: 'naver', // Usually your GitHub org/user name. 23 | projectName: 'airport', // Usually your repo name. 24 | 25 | onBrokenLinks: 'warn', 26 | onBrokenMarkdownLinks: 'warn', 27 | 28 | // Even if you don't use internationalization, you can use this field to set 29 | // useful metadata like html lang. For example, if your site is Chinese, you 30 | // may want to replace "en" with "zh-Hans". 31 | i18n: { 32 | defaultLocale: 'en', 33 | locales: ['en'], 34 | }, 35 | 36 | presets: [ 37 | [ 38 | 'classic', 39 | { 40 | docs: { 41 | sidebarPath: './sidebars.ts', 42 | }, 43 | blog: false, 44 | theme: { 45 | customCss: './src/css/custom.css', 46 | }, 47 | } satisfies Preset.Options, 48 | ], 49 | ], 50 | 51 | themeConfig: { 52 | // Replace with your project's social card 53 | image: 'img/airport-logo.png', 54 | navbar: { 55 | title: 'Airport', 56 | logo: { 57 | alt: 'Airport', 58 | src: 'img/airport-logo.png', 59 | }, 60 | items: [ 61 | { 62 | type: 'docSidebar', 63 | sidebarId: 'tutorialSidebar', 64 | position: 'left', 65 | label: 'Docs', 66 | }, 67 | { 68 | href: 'https://github.com/naver/airport', 69 | label: 'GitHub', 70 | position: 'right', 71 | }, 72 | ], 73 | }, 74 | footer: { 75 | style: 'dark', 76 | copyright: `Icons created by Vectorslab - Flaticon
Copyright © ${new Date().getFullYear()} NAVER Corp`, 77 | }, 78 | prism: { 79 | theme: prismThemes.github, 80 | darkTheme: prismThemes.dracula, 81 | }, 82 | } satisfies Preset.ThemeConfig, 83 | }; 84 | 85 | export default config; 86 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Airport 2 | Copyright 2024-present NAVER Corp. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | 22 | -------------------------------------------------------------------------------------- 23 | 24 | This project contains subcomponents with separate copyright notices and license terms. 25 | Your use of the source code for these subcomponents is subject to the terms and conditions of the following licenses. 26 | 27 | ===== 28 | 29 | facebook/docusaurus 30 | https://github.com/facebook/docusaurus 31 | 32 | 33 | MIT License 34 | 35 | Copyright (c) Facebook, Inc. and its affiliates. 36 | 37 | Permission is hereby granted, free of charge, to any person obtaining a copy 38 | of this software and associated documentation files (the "Software"), to deal 39 | in the Software without restriction, including without limitation the rights 40 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 41 | copies of the Software, and to permit persons to whom the Software is 42 | furnished to do so, subject to the following conditions: 43 | 44 | The above copyright notice and this permission notice shall be included in all 45 | copies or substantial portions of the Software. 46 | 47 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 48 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 49 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 50 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 51 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 52 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 53 | SOFTWARE. 54 | 55 | ===== 56 | -------------------------------------------------------------------------------- /packages/react/src/__tests__/useAirport-translation.test.tsx: -------------------------------------------------------------------------------- 1 | // Airport 2 | // Copyright 2024-present NAVER Corp. 3 | // MIT License 4 | 5 | import * as React from 'react' 6 | import { screen } from '@testing-library/react' 7 | import '@testing-library/jest-dom' 8 | 9 | import { useAirport } from '../useAirport' 10 | import { customRender } from './test-utils' 11 | 12 | function TestButton() { 13 | const { t, airport } = useAirport() 14 | const testLS = airport.createLS({ 15 | buttonTitle: { 16 | ko: '테스트 버튼', 17 | en: 'test button', 18 | }, 19 | }) 20 | 21 | return 22 | } 23 | 24 | function TestNav() { 25 | const { t, airport } = useAirport() 26 | const testLS = airport.createLS({ 27 | navTitle: { 28 | ko: '테스트 네비게이션 {button} !!', 29 | en: 'test navigation {button} !!' 30 | } 31 | }) 32 | 33 | return 34 | } 35 | 36 | function TestSection() { 37 | const { t, airport } = useAirport() 38 | const testLS = airport.createLS({ 39 | sectionTitle: { 40 | ko: '테스트 섹션 {content} !!', 41 | en: 'test section {content} !!' 42 | }, 43 | headline: { 44 | ko: '헤드라인', 45 | en: 'headline', 46 | } 47 | }) 48 | 49 | return
{t(testLS.sectionTitle, { content:

{t(testLS.headline)}

})}
50 | } 51 | 52 | function TestDiv({ isImp }: { isImp: boolean }) { 53 | const { t, airport } = useAirport() 54 | const LS = airport.createLS({ 55 | divTitle: { 56 | ko: `영역 {isImp ? '노출' : '미노출'}`, 57 | en: `{isImp ? 'expose' : 'unexpose'} area`, 58 | }, 59 | }) 60 | 61 | return
{t(LS.divTitle, { isImp })}
62 | } 63 | 64 | test('translation test (ko)', async () => { 65 | customRender(, 'ko') 66 | await screen.findByRole('button') 67 | expect(screen.getByRole('button')).toHaveTextContent('테스트 버튼') 68 | }) 69 | 70 | test('translation test (en)', async () => { 71 | customRender(, 'en') 72 | await screen.findByRole('button') 73 | expect(screen.getByRole('button')).toHaveTextContent('test button') 74 | }) 75 | 76 | test('component parameter support of translation function', async () => { 77 | customRender(, 'ko') 78 | await screen.findByRole('navigation') 79 | expect(screen.getByRole('button')).toHaveTextContent('테스트 버튼') 80 | }) 81 | 82 | test('element parameter support of translation function', async () => { 83 | customRender(, 'ko') 84 | await screen.findByTestId('test-section') 85 | expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent('헤드라인') 86 | }) 87 | 88 | test('ternary operator parameter support of translation function', async () => { 89 | customRender(, 'ko') 90 | await screen.findByTestId('test-div') 91 | expect(screen.getByTestId('test-div')).toHaveTextContent('영역 노출') 92 | }) 93 | -------------------------------------------------------------------------------- /packages/clean-ls-loader/index.js: -------------------------------------------------------------------------------- 1 | const ts = require('typescript') 2 | 3 | /** 4 | * 5 | * createLS( 6 | * { // lsNode 7 | * test: { // lsFieldNode 8 | * en: 'test', // langNode 9 | * }, 10 | * } 11 | * ) 12 | */ 13 | 14 | module.exports = function (source) { 15 | if (!process.env.AIRPORT_CLEAN_LS_LANGS_TO_REMOVE) return source 16 | if (source.indexOf('createLS') === -1) return source 17 | 18 | const fnName = process.env.AIRPORT_CLEAN_LS_FN_NAME ?? 'createLS' 19 | const langsToRemove = process.env.AIRPORT_CLEAN_LS_LANGS_TO_REMOVE.split(',') 20 | 21 | // Transformer function to modify the AST 22 | const transformer = context => { 23 | const visit = node => { 24 | // Check if node is a call expression: createLS({...}) 25 | if ( 26 | ts.isCallExpression(node) && 27 | ts.isIdentifier(node.expression) && 28 | node.expression?.text === fnName && 29 | node.arguments.length === 1 && 30 | ts.isObjectLiteralExpression(node.arguments[0]) 31 | ) { 32 | const lsNode = node.arguments[0] 33 | 34 | const newProperties = lsNode.properties.map(lsFieldNode => { 35 | if (!ts.isPropertyAssignment(lsFieldNode) || !ts.isObjectLiteralExpression(lsFieldNode.initializer)) 36 | return lsFieldNode 37 | 38 | // Decide whether to remove the langNode 39 | const cleanedInnerProperties = lsFieldNode.initializer.properties.filter(langNode => { 40 | if (ts.isPropertyAssignment(langNode) && ts.isIdentifier(langNode.name)) { 41 | return !langsToRemove.includes(langNode.name?.text) 42 | } 43 | return true 44 | }) 45 | 46 | // Return a new property with the cleaned inner object 47 | const cleaned = ts.factory.updatePropertyAssignment( 48 | lsFieldNode, 49 | lsFieldNode.name, 50 | ts.factory.updateObjectLiteralExpression(lsFieldNode.initializer, cleanedInnerProperties), 51 | ) 52 | return cleaned 53 | }) 54 | 55 | // Return a new call expression with the modified outer object 56 | return ts.factory.updateCallExpression(node, node.expression, node.typeArguments, [ 57 | ts.factory.updateObjectLiteralExpression(lsNode, newProperties), 58 | ]) 59 | } 60 | 61 | // Visit child nodes recursively 62 | return ts.visitEachChild(node, visit, context) 63 | } 64 | 65 | return node => ts.visitNode(node, visit) 66 | } 67 | 68 | // Parse the source code into an AST 69 | const sourceFile = ts.createSourceFile(this.resourcePath, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX) 70 | const result = ts.transform(sourceFile, [transformer]) 71 | const printer = ts.createPrinter({ 72 | newLine: ts.NewLineKind.LineFeed, 73 | removeComments: false, 74 | }) 75 | const transformedSource = printer.printFile(result.transformed[0]) 76 | 77 | return transformedSource 78 | } 79 | -------------------------------------------------------------------------------- /packages/react/src/useAirport.tsx: -------------------------------------------------------------------------------- 1 | // Airport 2 | // Copyright 2024-present NAVER Corp. 3 | // MIT License 4 | 5 | import * as React from 'react' 6 | import { LS, LSO, PartialLSO, PartialLS } from 'airport-js' 7 | 8 | import { AirportContext, AirportContextType } from './AirportProvider' 9 | 10 | /** 11 | * **Custom hook for convenient use of airport instance in react** 12 | * 13 | * @typeParam `T` - Array of supported locales 14 | * @typeParam `G` - Type of global language set 15 | */ 16 | export function useAirport, G extends LS | PartialLS>() { 17 | const airportContext = React.useContext>(AirportContext as any) 18 | if (!airportContext) { 19 | throw new Error('useAirport must be used within a AirportProvider') 20 | } 21 | const { airport, initialOptions, setLocale } = airportContext 22 | const [state, setState] = React.useState(getNewAirportState()) 23 | 24 | const { fn, fc } = airport 25 | 26 | React.useEffect(() => { 27 | if (state.locale !== airport.getLocale()) { 28 | setState(getNewAirportState()) 29 | } 30 | }) 31 | 32 | function getNewAirportState() { 33 | return { 34 | locale: airport ? airport.getLocale() : null, 35 | language: airport ? airport.getLanguage() : null, 36 | region: airport ? airport.getRegion() : null, 37 | } 38 | } 39 | 40 | function getTranslationElements( 41 | translated: string, 42 | elementVarEntries: [string, any][], 43 | entriesIdx: number, 44 | ): React.ReactNode[] { 45 | if (entriesIdx === elementVarEntries.length) return [translated] 46 | 47 | const key = elementVarEntries[entriesIdx][0] 48 | const element = elementVarEntries[entriesIdx][1] 49 | const parts = translated.split(new RegExp(`\\{${key}\\}`, 'gi')) 50 | const result: React.ReactNode[] = [] 51 | 52 | parts.forEach((part, index) => { 53 | const splitPart = getTranslationElements(part, elementVarEntries, entriesIdx + 1) 54 | result.push(...splitPart) 55 | if (index < parts.length - 1) { 56 | result.push(element) 57 | } 58 | }) 59 | 60 | return result 61 | } 62 | 63 | function t(lso: LSO, variableMap?: any, _forcedLocale?: L[number]): string 64 | function t(partialLso: PartialLSO, variableMap?: any, _forcedLocale?: L[number]): string 65 | function t(globalLSKey: keyof G, variableMap?: any, _forcedLocale?: L[number]): string 66 | function t(stringKey: string, variableMap?: any, _forcedLocale?: L[number]): string 67 | function t( 68 | lsoOrGlobalLSKey: LSO | PartialLSO | keyof G | string, 69 | variableMap?: any, 70 | _forcedLocale?: L[number], 71 | ): string | React.ReactNode { 72 | const elementVarEntries = Object.entries(variableMap ?? {}).filter(([_, value]) => React.isValidElement(value)) 73 | 74 | if (elementVarEntries.length) { 75 | const nonElementVariableMap = Object.fromEntries( 76 | Object.entries(variableMap ?? {}).filter(([_, value]) => !React.isValidElement(value)), 77 | ) 78 | const nonElementTranslation = airport.t(lsoOrGlobalLSKey as any, nonElementVariableMap, _forcedLocale) 79 | 80 | const translationElements = getTranslationElements(nonElementTranslation, elementVarEntries, 0) 81 | return ( 82 | <> 83 | {translationElements.map((element, index) => ( 84 | {element} 85 | ))} 86 | 87 | ) 88 | } 89 | 90 | return airport.t(lsoOrGlobalLSKey as any, variableMap, _forcedLocale) 91 | } 92 | 93 | return { 94 | airport, 95 | initialOptions, 96 | setLocale, 97 | t, 98 | fn, 99 | fc, 100 | ...state, 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /packages/js/src/types.ts: -------------------------------------------------------------------------------- 1 | // Airport 2 | // Copyright 2024-present NAVER Corp. 3 | // MIT License 4 | 5 | type AtLeastOneRequired = { 6 | [Key in keyof T]-?: Required> & Partial> 7 | }[keyof T] 8 | 9 | export interface Options, G extends LS | PartialLS> { 10 | supportedLocales: L 11 | locale: L[number] 12 | fallbackLocale: L[number] 13 | name?: string 14 | globalLS?: G 15 | currency?: LocaleMap 16 | currencyFormatValueKey?: string 17 | currencyFormat?: CurrencyMap 18 | keyCurrency?: CurrencyType 19 | exchangeRate?: CurrencyMap 20 | } 21 | 22 | export type RoundingMode = 'ceil' | 'floor' | 'round' 23 | 24 | /** 25 | * **Extended type to support 'roundingMode' option** 26 | * 27 | * roundingMode - this option supports only three modes('ceil', 'floor', 'round') 28 | */ 29 | export interface ImprovedNumberFormatOptions extends Omit { 30 | roundingMode?: RoundingMode 31 | } 32 | 33 | /** 34 | * **Map type to store currency-specific values or formats** 35 | * 36 | * @typeParam `T` - Specifies the type of value to be used by currency 37 | */ 38 | export type CurrencyMap = { 39 | [currency in CurrencyType]?: T 40 | } 41 | 42 | /** 43 | * **Map type to store locale-specific values** 44 | * 45 | * @typeParam `T` - Array of supported locales 46 | */ 47 | export type LocaleMap, V> = { 48 | [locale in T[number]]: V 49 | } 50 | 51 | /** 52 | * **LSO aggregation** 53 | * 54 | * **This type must consist of LSOs(type that has their own translation phrase by supported locale)** 55 | * 56 | * @typeParam `T` - Array of supported locales 57 | */ 58 | export interface LS> { 59 | [LSID: string]: LSO 60 | } 61 | 62 | /** 63 | * **PartialLSO aggregation** 64 | * 65 | * **PartialLS consists of optional LSOs** 66 | * 67 | * @typeParam `T` - Array of supported locales 68 | */ 69 | export interface PartialLS> { 70 | [LSID: string]: PartialLSO 71 | } 72 | 73 | /** 74 | * **Language Set Object type consist of translation phrase by locale** 75 | * 76 | * **LSO must have translations for all supported locale** 77 | * 78 | * @typeParam `T` - Array of supported locales 79 | */ 80 | export type LSO> = LocaleMap 81 | 82 | /** 83 | * **Partial Language Set Object type must have translation for at least one supported locale** 84 | * 85 | * @typeParam `T` - Array of supported locales 86 | */ 87 | export type PartialLSO> = AtLeastOneRequired> 88 | 89 | export type CurrencyType = Currency | string 90 | export enum Currency { 91 | KRW = 'KRW', 92 | JPY = 'JPY', 93 | USD = 'USD', 94 | } 95 | 96 | declare function t, G extends LS = {}>( 97 | lso: LSO, 98 | variableMap?: any, 99 | _forcedLocale?: T[number], 100 | ): string 101 | declare function t, G extends LS = {}>( 102 | partialLso: PartialLSO, 103 | variableMap?: any, 104 | _forcedLocale?: T[number], 105 | ): string 106 | declare function t, G extends LS = {}>( 107 | globalLSKey: keyof G, 108 | variableMap?: any, 109 | _forcedLocale?: T[number], 110 | ): string 111 | declare function t, G extends LS = {}>( 112 | stringKey: string, 113 | variableMap?: any, 114 | _forcedLocale?: T[number], 115 | ): string 116 | declare function t, G extends LS = {}>( 117 | lsoOrGlobalLSKey: LSO | keyof G | string, 118 | variableMap?: any, 119 | _forcedLocale?: T[number], 120 | ): string 121 | 122 | export type AirportT = typeof t 123 | -------------------------------------------------------------------------------- /packages/docs/docs/Airport/introduction.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | # Introduction 5 | ## TypeScript based module 6 | 7 | `airport` supports strong typings with Typescript. 8 | 9 | 10 | ## React binding module support 11 | 12 | By using `airport-react`, you can use `Airport` made for React. `Airport` also includes `Subtree` feature that you can use to set separate locale to some part of the component tree. 13 | 14 | Airport Logo 15 | 16 | 17 | ## Code splitting of language set 18 | 19 | - Each message is saved in a format called LSO(Language Set Object) and grouped and used as LS(Language Set). 20 | - Save messages in each file where they are used. Then messages will be Code splitted with related components. 21 | 22 | ```tsx 23 | // Language Set (LS) 24 | const LS = createLS({ 25 | // Language Set Object (LSO) 26 | hello: { 27 | ko: '안녕하세요', 28 | ja: 'こんにちは。', 29 | en: 'Hello', 30 | }, 31 | goodbye: { 32 | ko: '안녕히 가세요.', 33 | ja: 'さようなら。', 34 | en: 'Good bye', 35 | }, 36 | }) 37 | 38 | airport.t(LS.hello) 39 | ``` 40 | 41 | At the time of airport instantiation, you can pass a global LS that can be referenced anywhere in the project. If you use XLT, filtering can be done using LS format and provide it as an instance option. 42 | 43 | 44 | ```tsx 45 | // Static Messages 46 | const globalLS = createLS({ 47 | home: { 48 | ko: '홈', 49 | ja: 'ホーム', 50 | en: 'Home', 51 | } 52 | }) 53 | 54 | const airport = new Airport({ 55 | // ... 56 | globalLS, 57 | }) 58 | 59 | airport.t('hello') 60 | ``` 61 | 62 |
63 | 64 | ## Easy to customize 65 | 66 | Using `airport`, you can simply add wrappers to elaborate and add features you need. 67 | 68 | ### Sample feature: Display LS key 69 | 70 | There are cases when Product Managers or other non-developers need to update language sets. 71 | In theses cases, it is hard to identify which key to update without actually looking into the code. 72 | Following the sample code that shows LS key in place. 73 | 74 | - `${YOUR_DOMAIN}?_SHOW_LS_ID_` 75 | 76 | ![image](./img/lsKeyDisplay.png) 77 | 78 |
79 | Sample Code 80 | 81 | ```ts 82 | import { useAirport as _useAirport, LocaleMap } from 'airport-react' 83 | import { AirportType, LocaleType } from '../const/airport' 84 | import { GlobalLSType } from '../const/airport/globalLS' 85 | 86 | export const useAirport = () => { 87 | const airport = _useAirport() 88 | 89 | const t: typeof airport.t = (...args: any[]) => { 90 | const [lsoOrGlobalLSKey, variableMap, forcedLocale] = args 91 | const translatedText = airport.t(lsoOrGlobalLSKey as LocaleMap, variableMap, forcedLocale) 92 | if (typeof window === 'undefined') return translatedText 93 | 94 | let lsKey = lsoOrGlobalLSKey?.__KEY__ ?? '' 95 | let lsID = lsKey.split('#').pop() 96 | 97 | const isGlobalKey = typeof lsoOrGlobalLSKey === 'string' 98 | if (isGlobalKey && !lsKey) { 99 | lsID = lsKey 100 | } 101 | const urlParams = new URLSearchParams(window.location.search) 102 | 103 | if (urlParams.has('_SHOW_LS_ID_')) return lsID ?? '' 104 | return translatedText 105 | } 106 | 107 | return { 108 | ...airport, 109 | t, 110 | airport: { ...airport.airport, t } as AirportType, 111 | } 112 | } 113 | 114 | ``` 115 | 116 |
117 | 118 | ## Number/Currency format customization based on `Intl` 119 | 120 | - Number/Currency is formatted using `Intl` 121 | - If you want to customize the format, it can be done manually instead of ICU(International Components for Unicode) or Unicode CLDR(Common Locale Data Repository) 122 | 123 | 124 | ```tsx 125 | airport.fn(1000) // Number formatting with Intl 126 | airport.fc(99000) // Currency formatting. Customized format will be used if passed as an option at instantiation. 127 | ``` 128 | 129 |
130 | 131 | ## Currency conversion support 132 | 133 | - If you set exchange rate and currency for each locale, currency exchange will be applied with relative format. 134 | 135 | 136 | ```tsx 137 | const airport = new Airport({ 138 | supportedLocales, 139 | locale: 'ko-KR', 140 | fallbackLocale: 'ko-KR', 141 | currency: { 142 | 'ko-KR': Currency.KRW, 143 | 'en-US': Currency.USD, 144 | 'ja-JP': Currency.JPY, 145 | }, 146 | keyCurrency: Currency.USD, // Standard currency. Default is USD 147 | exchangeRate: { 148 | [Currency.USD]: 1, 149 | [Currency.KRW]: 1135.50, 150 | [Currency.JPY]: 104.34, 151 | } 152 | }) 153 | 154 | // Custom currency Yen is passed 155 | const price = airport.fc(10000, Currenty.JPY) 156 | // Prints in current locale's currency 157 | console.log(price) // "₩108,827" 158 | ``` 159 | 160 |
161 | 162 | ## VanillaJS support 163 | 164 | You can use Airport without ReactJS. See `Installation > Use airport with vanillaJS` 165 | 166 | -------------------------------------------------------------------------------- /packages/docs/docs/Airport/API.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 3 3 | --- 4 | 5 | # API 6 | 7 | ### `getOptions()` 8 | Retrieves current airport instance's options. 9 | 10 | **Arguments** 11 | - none 12 | 13 | **Returns** 14 | - `(Option)`: current instance's option 15 | 16 | ```ts 17 | const options = airport.getOptions() 18 | ``` 19 | 20 | 21 | ### `getLocale()` 22 | Retrieves current locale. 23 | 24 | **Arguments** 25 | - none 26 | 27 | **Returns** 28 | - `(LocaleText)`: current locale in string 29 | 30 | ```ts 31 | const locale = airport.getLocale() 32 | ``` 33 | 34 | 35 | ### `getLanguage()` 36 | Retrieves current language. 37 | 38 | **Arguments** 39 | - none 40 | 41 | **Returns** 42 | - `(string)`: current language in string 43 | 44 | ```ts 45 | const language = airport.getLanguage() 46 | ``` 47 | 48 | 49 | ### `getRegion()` 50 | Retrieves current region. 51 | 52 | **Arguments** 53 | - none 54 | 55 | **Returns** 56 | - `(string)`: current region in string 57 | 58 | ```ts 59 | const region = airport.getRegion() 60 | ``` 61 | 62 | 63 | ### `changeLocale(locale: LocaleText)` 64 | Change locale to desired locale. 65 | 66 | **Arguments** 67 | - `locale(LocaleText)`: Any entry in `supportedLocales` to change locale to. 68 | 69 | **Returns** 70 | - none 71 | 72 | ```ts 73 | const newLocale = `ko` 74 | airport.changeLocale(newLocale) 75 | ``` 76 | 77 | 78 | ### `t(ls, variableMap?: Record, _forcedLocale?: LocaleText)` 79 | Function that selects appropriate text from language set according to current locale. Dynamic values can be applied by passing `variableMap` as a second parameter. 80 | 81 | **Arguments** 82 | - `ls` can be any of the types below: 83 | - `lso(LSO)`: Language Set Object(LSO) that has values for all supported locales. 84 | - `partialLso(PartialLSO)`: Language Set Object that has values for at least 1 supported locales. 85 | - `globalLSKey(keyof G)`: key of global Langauge Set Object. 86 | - `variableMap(Record)`(optional): Key-value object that has values to substitute from text. 87 | - `_forcedLocale(LocaleText)`(optional): locale to apply instead of current airport instance's locale. 88 | 89 | **Returns** 90 | - `(string)`: Current locale's text from `ls`. Returns `''` if current locale entry doesn't exist in `ls`. 91 | 92 | ```ts 93 | // lso 94 | const createLS = createLSFactory() 95 | const LS = createLS({ 96 | helloFull: { 97 | ko: '안녕하세요', 98 | en: 'Hello', 99 | }, 100 | }) 101 | console.log(airport.t(LS.helloFull)) 102 | 103 | // partialLSO 104 | export const createPartialLS = createLSFactory() 105 | const PartialLS = createPartialLS({ 106 | helloPartial: { 107 | en: 'Hello', 108 | } 109 | }) 110 | console.log(airport.t(LS.helloPartial)) 111 | 112 | // globalLSKey 113 | // Assume that airport has been initialized with global LSO that has 'hello' entry. 114 | console.log(airport.t('hello')) 115 | 116 | // dynamic variable 117 | export const createPartialLS = createLSFactory() 118 | const dynamicVarLS = createPartialLS({ 119 | hello: { 120 | en: 'Hello, {name}', 121 | } 122 | }) 123 | console.log(airport.t(dynamicVarLS.hello, { name: 'Jun' })) 124 | 125 | // forced locale 126 | export const createLS = createLSFactory() 127 | const LS = createLS({ 128 | hello: { 129 | ko: '안녕하세요', 130 | en: 'Hello', 131 | } 132 | }) 133 | console.log(airport.t(LS.hello, undefined, 'ko')) 134 | ``` 135 | 136 | 137 | ### `fn(value: number, options?: ImprovedNumberFormatOptions, _forcedLocale?: T[number])` 138 | Formats given number value appropriate to current locale 139 | 140 | **Arguments** 141 | - `value(number)`: number to format 142 | - `options(ImprovedNumberFormatOptions)`(optional): options for format with extra options added from `Intl.NumberFormatOptions` 143 | - `_forcedLocale(LocaleText)`(optional): custom locale to apply instead of current locale. 144 | 145 | **Returns** 146 | - `(string)`: formatted number in current locale's number format. 147 | 148 | ```ts 149 | console.log(airport.fn(10000)) 150 | ``` 151 | 152 | 153 | 154 | ### `fc(value: number, customFormat?: string, baseCurrency?: Currency, isFixedCurrency = false, _forcedLocale?: T[number])` 155 | Formats given number to current locale's currency. 156 | (Uses `Option.currencyMap`, `Option.currencyFormatValueKey`,`Option.currencyFormat`) 157 | 158 | **Arguments** 159 | - `value(number)`: number to format as currency. 160 | - `customFormat(string)`(optional): custom format to apply instead of `Option.currencyFormat`. 161 | - `baseCurrency(Currency)`(optional): currency of `value`. `baseCurrency` is required if `isFixedCurrency` is `true`. 162 | - `isFixedCurrency(boolean)`(optional): if `isFixedCurrency` is true, `value` will not be exchanged and formatted to current locale's currency. 163 | - `_forcedLocale(LocaleText)`(optional): custom locale to apply instead of current locale. 164 | 165 | **Returns** 166 | - `(string)`: formatted number in current locale's currency. 167 | 168 | ```ts 169 | // Assume that airport has been constructed with following option: 170 | // 171 | // locale: 'ko-KR', 172 | // currency: { 173 | // 'ko-KR': 'KRW', 174 | // 'en-US': 'USD', 175 | // }, 176 | // currencyFormat: { 177 | // 'USD': 'USD {v}', 178 | // 'KRW': 'KRW {v}, 179 | // }, 180 | // keyCurrency: Currency.USD, 181 | // exchangeRate: { 182 | // [Currency.USD]: 1, 183 | // [Currency.KRW]: 1000, 184 | // } 185 | 186 | // KRW 10,000 187 | console.log(airport.fc(10000)) 188 | // KRW 10,000,000 189 | console.log(airport.fc(10000, undefined, USD)) 190 | // USD 10,000 191 | console.log(airport.fc(10000, undefined, USD, true)) 192 | ``` 193 | -------------------------------------------------------------------------------- /packages/js/src/__tests__/Airport-translation.test.ts: -------------------------------------------------------------------------------- 1 | // Airport 2 | // Copyright 2024-present NAVER Corp. 3 | // MIT License 4 | 5 | import { Airport } from '../Airport' 6 | import { createLSFactory } from '../utils' 7 | 8 | describe('Airport translation functions test', () => { 9 | const supportedLocales = ['ko', 'en', 'en-GB', 'ja', 'jh-CN'] as const 10 | type LocaleType = typeof supportedLocales 11 | const createLS = createLSFactory() 12 | const createPartialLS = createLSFactory() 13 | const globalLS = createLS({ 14 | hello: { 15 | ko: '안녕하세요.', 16 | en: 'Hello.', 17 | 'en-GB': 'Hello.', 18 | ja: 'こんにちは。', 19 | 'jh-CN': '你好。', 20 | }, 21 | }) 22 | 23 | let airport: Airport 24 | 25 | beforeEach(() => { 26 | airport = new Airport({ 27 | supportedLocales, 28 | globalLS, 29 | locale: 'ko', 30 | fallbackLocale: 'ko', 31 | }) 32 | }) 33 | 34 | test('airport.t() can translate LSO', () => { 35 | const LS = airport.createLS({ 36 | hello: { 37 | ko: '안녕하세요, 이것은 색입니다.', 38 | en: 'Hello, This is color.', 39 | 'en-GB': 'Hello, This is colour.', 40 | ja: 'こんにちは, これは色です。', 41 | 'jh-CN': '你好,这是颜色。', 42 | }, 43 | }) 44 | expect(airport.t(LS.hello)).toBe('안녕하세요, 이것은 색입니다.') 45 | }) 46 | 47 | test('airport.t() can translate after locale change', () => { 48 | const LS = airport.createLS({ 49 | introduce: { 50 | ko: '안녕하세요, 저는 20살입니다.', 51 | en: 'Hello, I am 20 years old.', 52 | 'en-GB': 'Hello, I am 20 years old.', 53 | ja: 'こんにちは、私は20歳です。', 54 | 'jh-CN': '你好,我20岁。', 55 | }, 56 | }) 57 | 58 | expect(airport.t(LS.introduce)).toBe('안녕하세요, 저는 20살입니다.') 59 | airport.changeLocale('en-GB') 60 | expect(airport.t(LS.introduce)).toBe('Hello, I am 20 years old.') 61 | airport.changeLocale('en-KR' as 'en') 62 | expect(airport.t(LS.introduce)).toBe('Hello, I am 20 years old.') 63 | }) 64 | 65 | test('airport.t() can inject variables with evaluating js expression', () => { 66 | const LS = airport.createLS({ 67 | hello: { 68 | ko: '{name}님, 사과 {count}개 주세요.', 69 | en: '{name}, please give me {count} apple{count === 0 ? "" : "s"}.', 70 | 'en-GB': '{name}, please give me {count} apple{count === 0 ? "" : "s"}.', 71 | ja: '{name}様、リンゴ{count}個ください。', 72 | 'jh-CN': '{name}先生,请给我{count}个苹果。', 73 | }, 74 | }) 75 | 76 | expect(airport.t(LS.hello, { name: '서준', count: 3 })).toBe('서준님, 사과 3개 주세요.') 77 | airport.changeLocale('en') 78 | expect(airport.t(LS.hello, { name: 'Jack', count: 3 })).toBe('Jack, please give me 3 apples.') 79 | }) 80 | 81 | test('airport.t() can inject variables with evaluating nested js expression', () => { 82 | const LS = airport.createLS({ 83 | hello: { 84 | ko: '{name}님, 사과 {count}개 주세요.', 85 | en: '{name}, please give me {count} {count > 100 ? "{level}" : ""} apple{count === 0 ? "" : "s"}.', 86 | 'en-GB': '{name}, please give me {count} apple{count === 0 ? "" : "s"}.', 87 | ja: '{name}様、リンゴ{count}個ください。', 88 | 'jh-CN': '{name}先生,请给我{count}个苹果。', 89 | }, 90 | }) 91 | 92 | airport.changeLocale('en') 93 | expect(airport.t(LS.hello, { name: 'Jack', level: 'super', count: 120 })).toBe( 94 | 'Jack, please give me 120 super apples.', 95 | ) 96 | }) 97 | 98 | test('airport.t() can inject variables when key is number', () => { 99 | const LS = airport.createLS({ 100 | hello: { 101 | ko: '{0}님, 안녕하세요.', 102 | en: 'Hello, {0}.', 103 | 'en-GB': 'Hello, {0}', 104 | ja: '{0}さん, こんにちは。', 105 | 'jh-CN': '{0}先生,你好。', 106 | }, 107 | }) 108 | 109 | airport.changeLocale('en') 110 | expect(airport.t(LS.hello, { 0: 'Mason' })).toBe('Hello, Mason.') 111 | }) 112 | 113 | test('airport.t() can translate options.globalLS', () => { 114 | expect(airport.t('hello')).toBe('안녕하세요.') 115 | 116 | airport.changeLocale('en-KR' as 'en') 117 | 118 | expect(airport.t('hello')).toBe('Hello.') 119 | }) 120 | 121 | test('airport.t() returns key when key is string and it is not the key of globaLS', () => { 122 | expect(airport.t('존재하지 않는 키' as 'hello')).toBe('존재하지 않는 키') 123 | }) 124 | 125 | test('airport.t() can use forcedLocale', () => { 126 | expect(airport.getLocale()).toBe('ko') 127 | expect(airport.t('hello', null, 'en')).toBe('Hello.') 128 | }) 129 | 130 | test(`airport uses fallbackLocale if current locale doesn't exist`, () => { 131 | const airport = new Airport({ 132 | supportedLocales, 133 | globalLS, 134 | locale: 'ja', 135 | fallbackLocale: 'ko', 136 | }) 137 | 138 | const LS = createPartialLS({ 139 | onlyKoEn: { 140 | ko: 'korean', 141 | en: 'english', 142 | }, 143 | }) 144 | 145 | expect(airport.t(LS.onlyKoEn)).toBe('korean') 146 | }) 147 | 148 | test(`airport outputs empty string if both current locale and fallbackLocale don't exist`, () => { 149 | const airport = new Airport({ 150 | supportedLocales, 151 | globalLS, 152 | locale: 'ja', 153 | fallbackLocale: 'ko', 154 | }) 155 | 156 | const LS = createPartialLS({ 157 | onlyEn: { 158 | en: 'english', 159 | }, 160 | }) 161 | 162 | expect(airport.t(LS.onlyEn)).toBe('') 163 | }) 164 | 165 | test('airport should not cause any errors related to curly braces', () => { 166 | const airport = new Airport({ 167 | supportedLocales, 168 | globalLS, 169 | locale: 'ja', 170 | fallbackLocale: 'ko', 171 | }) 172 | 173 | const LS = createPartialLS({ 174 | onlyKo: { 175 | ko: `형식: {"test": {"title": "", "alt": ""}, ...}`, 176 | }, 177 | }) 178 | 179 | expect(airport.t(LS.onlyKo)).toBe('형식: {"test": {"title": "", "alt": ""}, ...}') 180 | }) 181 | 182 | 183 | test('Airport supports partialLS', () => { 184 | const partialLS = createPartialLS({ 185 | koTest: { ko: '부분 언어 집합' }, 186 | enTest: { en: 'This is english LS'} 187 | }) 188 | 189 | expect(airport.t(partialLS.koTest)).toBe('부분 언어 집합') 190 | expect(airport.t(partialLS.enTest)).toBe("") 191 | }) 192 | }) 193 | -------------------------------------------------------------------------------- /packages/js/src/__tests__/Airport.test.ts: -------------------------------------------------------------------------------- 1 | // Airport 2 | // Copyright 2024-present NAVER Corp. 3 | // MIT License 4 | 5 | import { Airport } from '../Airport' 6 | import { Options } from '../types' 7 | 8 | describe('Airport Class', () => { 9 | const supportedLocales = ['ko', 'en', 'en-GB', 'ja', 'jh-CN'] as const 10 | const globalLS = { 11 | hello: { 12 | ko: '안녕하세요.', 13 | en: 'Hello.', 14 | 'en-GB': 'Hello.', 15 | ja: 'こんにちは。', 16 | 'jh-CN': '你好。', 17 | }, 18 | } 19 | 20 | let airport: Airport 21 | let enGBAirport: Airport 22 | beforeEach(() => { 23 | airport = new Airport({ 24 | supportedLocales, 25 | globalLS, 26 | locale: 'ko', 27 | fallbackLocale: 'ko', 28 | }) 29 | enGBAirport = new Airport({ 30 | supportedLocales, 31 | locale: 'en-GB', 32 | fallbackLocale: 'en-GB', 33 | }) 34 | }) 35 | 36 | test('Airport can analysis locale', () => { 37 | expect(airport.getLocale()).toBe('ko') 38 | expect(enGBAirport.getLocale()).toBe('en-GB') 39 | }) 40 | 41 | test('Airport can analysis language', () => { 42 | expect(airport.getLanguage()).toBe('ko') 43 | expect(enGBAirport.getLanguage()).toBe('en') 44 | }) 45 | 46 | test('Airport can analysis region', () => { 47 | expect(enGBAirport.getRegion()).toBe('GB') 48 | }) 49 | 50 | test('Airport returns "undefined" region value for locale code without region info', () => { 51 | expect(airport.getRegion()).toBe(undefined) 52 | }) 53 | 54 | test('Airport can change locale', () => { 55 | expect(airport.getLocale()).toBe('ko') 56 | expect(airport.getLanguage()).toBe('ko') 57 | expect(airport.getRegion()).toBe(undefined) 58 | 59 | airport.changeLocale('en-GB') 60 | 61 | expect(airport.getLocale()).toBe('en-GB') 62 | expect(airport.getLanguage()).toBe('en') 63 | expect(airport.getRegion()).toBe('GB') 64 | }) 65 | 66 | test('Airport can use language based locale though received language is supported but region is not', () => { 67 | airport.changeLocale('en-KR' as 'ja') // use 'as' for preventing ts error 68 | expect(airport.getLocale()).toBe('en-KR') 69 | expect(airport.getLanguage()).toBe('en') 70 | expect(airport.getRegion()).toBe('KR') 71 | }) 72 | 73 | test('Airport can use fallbackLocale when received language is not supported', () => { 74 | airport.changeLocale('de' as 'en') // use 'as' for preventing ts error 75 | expect(airport.getLocale()).toBe('ko') 76 | expect(airport.getLanguage()).toBe('ko') 77 | expect(airport.getRegion()).toBe(undefined) 78 | }) 79 | 80 | test('Airport can get options of airport instance', () => { 81 | const options: Options = { 82 | supportedLocales, 83 | globalLS, 84 | locale: 'ko', 85 | fallbackLocale: 'ko', 86 | } 87 | const localAirport = new Airport(options) 88 | expect(localAirport.getOptions()).toStrictEqual(options) 89 | }) 90 | }) 91 | 92 | describe('Airport number formatting test', () => { 93 | test('airport.fn() returns value based on Intl.NumberFormat', () => { 94 | const supportedLocales = ['ko', 'nl'] as const 95 | const localAirport = new Airport({ 96 | supportedLocales, 97 | locale: 'ko', 98 | fallbackLocale: 'ko', 99 | }) 100 | 101 | const value = 100000 102 | expect(localAirport.fn(value)).toBe(new Intl.NumberFormat('ko').format(value)) 103 | 104 | localAirport.changeLocale('nl') 105 | 106 | expect(localAirport.fn(value)).toBe(new Intl.NumberFormat('nl').format(value)) 107 | }) 108 | 109 | test('airport.fn() can use forcedLocale', () => { 110 | const supportedLocales = ['ko', 'nl'] as const 111 | const localAirport = new Airport({ 112 | supportedLocales, 113 | locale: 'ko', 114 | fallbackLocale: 'ko', 115 | }) 116 | 117 | const value = 100000 118 | 119 | expect(localAirport.getLocale()).toBe('ko') 120 | expect(localAirport.fn(value, undefined, 'nl')).toBe(new Intl.NumberFormat('nl').format(value)) 121 | }) 122 | 123 | test('airport.fn() returns correct value when option is changed', () => { 124 | const supportedLocales = ['ko', 'nl'] as const 125 | const localAirport = new Airport({ 126 | supportedLocales, 127 | locale: 'ko', 128 | fallbackLocale: 'ko', 129 | }) 130 | 131 | const value = 0.335 132 | expect(localAirport.fn(value, { style: 'percent' })).toBe('34%') 133 | expect(localAirport.fn(value)).toBe('0.335') 134 | expect(localAirport.fn(value, { maximumFractionDigits: 2 })).toBe('0.34') 135 | expect(localAirport.fn(30.1634, { maximumSignificantDigits: 3 })).toBe('30.2') 136 | }) 137 | 138 | test('airport.fn() returns correct value when roundingMode option is given', () => { 139 | const supportedLocales = ['ko', 'nl'] as const 140 | const localAirport = new Airport({ 141 | supportedLocales, 142 | locale: 'ko', 143 | fallbackLocale: 'ko', 144 | }) 145 | 146 | const testNum1 = 0.33357 147 | expect(localAirport.fn(testNum1)).toBe('0.334') 148 | expect(localAirport.fn(testNum1, { roundingMode: 'ceil' })).toBe('0.334') 149 | expect(localAirport.fn(testNum1, { roundingMode: 'round' })).toBe('0.334') 150 | expect(localAirport.fn(testNum1, { roundingMode: 'floor' })).toBe('0.333') 151 | 152 | const testNum2 = 3055.5321234 153 | expect(localAirport.fn(testNum2, { maximumSignificantDigits: 2 })).toBe('3,100') 154 | expect(localAirport.fn(testNum2, { maximumSignificantDigits: 2, roundingMode: 'ceil' })).toBe('3,100') 155 | expect(localAirport.fn(testNum2, { maximumSignificantDigits: 2, roundingMode: 'round' })).toBe('3,100') 156 | expect(localAirport.fn(testNum2, { maximumSignificantDigits: 2, roundingMode: 'floor' })).toBe('3,000') 157 | 158 | const testNum3 = 3035.5321234 159 | expect(localAirport.fn(testNum3, { maximumSignificantDigits: 2 })).toBe('3,000') 160 | expect(localAirport.fn(testNum3, { maximumSignificantDigits: 2, roundingMode: 'ceil' })).toBe('3,100') 161 | expect(localAirport.fn(testNum3, { maximumSignificantDigits: 2, roundingMode: 'round' })).toBe('3,000') 162 | expect(localAirport.fn(testNum3, { maximumSignificantDigits: 2, roundingMode: 'floor' })).toBe('3,000') 163 | 164 | const testNum4 = -13.23455 165 | expect(localAirport.fn(testNum4, { maximumFractionDigits: 2 })).toBe('-13.23') 166 | expect(localAirport.fn(testNum4, { maximumFractionDigits: 2, roundingMode: 'ceil' })).toBe('-13.23') 167 | expect(localAirport.fn(testNum4, { maximumFractionDigits: 2, roundingMode: 'round' })).toBe('-13.23') 168 | expect(localAirport.fn(testNum4, { maximumFractionDigits: 2, roundingMode: 'floor' })).toBe('-13.24') 169 | 170 | const testNum5 = -13.23655 171 | expect(localAirport.fn(testNum5, { maximumFractionDigits: 2 })).toBe('-13.24') 172 | expect(localAirport.fn(testNum5, { maximumFractionDigits: 2, roundingMode: 'ceil' })).toBe('-13.23') 173 | expect(localAirport.fn(testNum5, { maximumFractionDigits: 2, roundingMode: 'round' })).toBe('-13.24') 174 | expect(localAirport.fn(testNum5, { maximumFractionDigits: 2, roundingMode: 'floor' })).toBe('-13.24') 175 | }) 176 | }) 177 | -------------------------------------------------------------------------------- /packages/js/src/__tests__/Airport-currency.test.ts: -------------------------------------------------------------------------------- 1 | // Airport 2 | // Copyright 2024-present NAVER Corp. 3 | // MIT License 4 | 5 | import { Airport } from '../Airport' 6 | import { Currency } from '../types' 7 | 8 | describe('Airport currency functions test', () => { 9 | const supportedLocales = ['ko-KR', 'en-US', 'ja-JP'] as const 10 | 11 | let airport: Airport 12 | beforeEach(() => { 13 | airport = new Airport({ 14 | supportedLocales, 15 | locale: 'ko-KR', 16 | fallbackLocale: 'ko-KR', 17 | currency: { 18 | 'ko-KR': Currency.KRW, 19 | 'en-US': Currency.USD, 20 | 'ja-JP': Currency.JPY, 21 | }, 22 | }) 23 | }) 24 | 25 | test('airport.fc() can format by locale and currency pair', () => { 26 | const value = 100000 27 | 28 | const koKrFormatted = airport.fc(value) 29 | const koKrKrwIntlFormatted = new Intl.NumberFormat('ko-KR', { style: 'currency', currency: 'KRW' }).format(value) 30 | 31 | expect(koKrFormatted).toBe(koKrKrwIntlFormatted) 32 | 33 | airport.changeLocale('en-US') 34 | 35 | const enUsFormatted = airport.fc(value) 36 | const enUsUsdIntlFormatted = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(value) 37 | 38 | expect(enUsFormatted).toBe(enUsUsdIntlFormatted) 39 | }) 40 | 41 | test('airport.fc() can use forcedLocale', () => { 42 | expect(airport.getLocale()).toBe('ko-KR') 43 | 44 | const value = 100000 45 | 46 | const enUsFormatted = airport.fc(value, undefined, undefined, undefined, 'en-US') 47 | const enUsUsdIntlFormatted = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(value) 48 | 49 | expect(enUsFormatted).toBe(enUsUsdIntlFormatted) 50 | }) 51 | 52 | test('airport.fc() can format with custom currencyFormat when it exists', () => { 53 | const localAirport = new Airport({ 54 | supportedLocales, 55 | locale: 'ko-KR', 56 | fallbackLocale: 'ko-KR', 57 | currency: { 58 | 'ko-KR': Currency.KRW, 59 | 'en-US': Currency.USD, 60 | 'ja-JP': Currency.JPY, 61 | }, 62 | currencyFormat: { 63 | [Currency.KRW]: '{v}원입니다.', 64 | [Currency.USD]: 'Price is ${v}.', 65 | }, 66 | }) 67 | 68 | const value = 100000 69 | 70 | const koKrLsBasedFormatted = localAirport.fc(value) 71 | const koKrIntlNumberFormatted = new Intl.NumberFormat('ko-KR').format(value) 72 | expect(koKrLsBasedFormatted).toBe(`${koKrIntlNumberFormatted}원입니다.`) 73 | 74 | localAirport.changeLocale('en-US') 75 | 76 | const enUsLsBasedFormatted = localAirport.fc(value) 77 | const enUsIntlNumberFormatted = new Intl.NumberFormat('en-US').format(value) 78 | expect(enUsLsBasedFormatted).toBe(`Price is $${enUsIntlNumberFormatted}.`) 79 | 80 | localAirport.changeLocale('ja-JP') 81 | 82 | const jaJpLsBasedFormatted = localAirport.fc(value) 83 | const jaJpIntlFormatted = new Intl.NumberFormat('ja-JP', { style: 'currency', currency: 'JPY' }).format(value) 84 | expect(jaJpLsBasedFormatted).toBe(jaJpIntlFormatted) 85 | }) 86 | 87 | test('airport.fc() can use custom currencyFormatValueKey', () => { 88 | const localAirport = new Airport({ 89 | supportedLocales, 90 | locale: 'ko-KR', 91 | fallbackLocale: 'ko-KR', 92 | currency: { 93 | 'ko-KR': Currency.KRW, 94 | 'en-US': Currency.USD, 95 | 'ja-JP': Currency.JPY, 96 | }, 97 | currencyFormatValueKey: 'price', 98 | currencyFormat: { 99 | [Currency.KRW]: '{price}원입니다.', 100 | }, 101 | }) 102 | 103 | const value = 100000 104 | 105 | const koKrLsBasedFormatted = localAirport.fc(value) 106 | const koKrIntlNumberFormatted = new Intl.NumberFormat('ko-KR').format(value) 107 | expect(koKrLsBasedFormatted).toBe(`${koKrIntlNumberFormatted}원입니다.`) 108 | }) 109 | 110 | test('airport.fc() can override format with customFormat as argument', () => { 111 | const localAirport = new Airport({ 112 | supportedLocales, 113 | locale: 'ko-KR', 114 | fallbackLocale: 'ko-KR', 115 | currency: { 116 | 'ko-KR': Currency.KRW, 117 | 'en-US': Currency.USD, 118 | 'ja-JP': Currency.JPY, 119 | }, 120 | currencyFormat: { 121 | [Currency.KRW]: '{v}원입니다.', 122 | [Currency.USD]: 'Price is ${v}.', 123 | }, 124 | }) 125 | 126 | const value = 100000 127 | 128 | const koKrLsBasedFormatted = localAirport.fc(value, '총 가격은 {v}원 입니다!') 129 | const koKrIntlNumberFormatted = new Intl.NumberFormat('ko-KR').format(value) 130 | expect(koKrLsBasedFormatted).toBe(`총 가격은 ${koKrIntlNumberFormatted}원 입니다!`) 131 | 132 | localAirport.changeLocale('en-US') 133 | 134 | const enUsLsBasedFormatted = localAirport.fc(value, 'Total price is {v}!') 135 | const enUsIntlNumberFormatted = new Intl.NumberFormat('en-US').format(value) 136 | expect(enUsLsBasedFormatted).toBe(`Total price is ${enUsIntlNumberFormatted}!`) 137 | 138 | localAirport.changeLocale('ja-JP') 139 | 140 | const jaJpLsBasedFormatted = localAirport.fc(value, '合計価格は{v}円です!') 141 | const jaJpIntlFormatted = new Intl.NumberFormat('ja-JP').format(value) 142 | expect(jaJpLsBasedFormatted).toBe(`合計価格は${jaJpIntlFormatted}円です!`) 143 | }) 144 | 145 | test('airport.fc() with baseCurrency can apply exchange.', () => { 146 | const usd = 1 147 | const usdToKrw = 1135.50 148 | const usdToJpy = 104.34 149 | 150 | const localAirport = new Airport({ 151 | supportedLocales, 152 | locale: 'ko-KR', 153 | fallbackLocale: 'ko-KR', 154 | currency: { 155 | 'ko-KR': Currency.KRW, 156 | 'en-US': Currency.USD, 157 | 'ja-JP': Currency.JPY, 158 | }, 159 | exchangeRate: { 160 | [Currency.USD]: usd, 161 | [Currency.KRW]: usdToKrw, 162 | [Currency.JPY]: usdToJpy, 163 | } 164 | }) 165 | 166 | const value = 100000 167 | 168 | const krwPrice = localAirport.fc(value, undefined, Currency.JPY) 169 | const exchangedValue = (value / (usdToJpy / usd)) * (usdToKrw / usd) 170 | const koKrKrwIntlFormatted = new Intl.NumberFormat('ko-KR', { style: 'currency', currency: 'KRW' }).format(exchangedValue) 171 | 172 | expect(krwPrice).toBe(koKrKrwIntlFormatted) 173 | }) 174 | 175 | test('airport.fc() with baseCurrency displays fixed currency when exchangeRate is not given.', () => { 176 | const usd = 1 177 | const usdToKrw = 1135.50 178 | 179 | const localAirport = new Airport({ 180 | supportedLocales, 181 | locale: 'ko-KR', 182 | fallbackLocale: 'ko-KR', 183 | currency: { 184 | 'ko-KR': Currency.KRW, 185 | 'en-US': Currency.USD, 186 | 'ja-JP': Currency.JPY, 187 | }, 188 | exchangeRate: { 189 | [Currency.USD]: usd, 190 | [Currency.KRW]: usdToKrw, 191 | } 192 | }) 193 | 194 | const value = 100000 195 | 196 | const krwPrice = localAirport.fc(value, undefined, Currency.JPY) 197 | const koKrKrwIntlFormatted = new Intl.NumberFormat('ko-KR', { style: 'currency', currency: 'JPY' }).format(value) 198 | 199 | expect(krwPrice).toBe(koKrKrwIntlFormatted) 200 | }) 201 | 202 | test('airport.fc() with baseCurrency can apply exchange by custom keyCurrency', () => { 203 | const krw = 1000 204 | const krwToJyp = 91.9887 205 | 206 | const supportedLocales = ['ko-KR', 'ja-JP'] as const 207 | const localAirport = new Airport({ 208 | supportedLocales, 209 | locale: 'ko-KR', 210 | fallbackLocale: 'ko-KR', 211 | currency: { 212 | 'ko-KR': Currency.KRW, 213 | 'ja-JP': Currency.JPY, 214 | }, 215 | keyCurrency: Currency.KRW, 216 | exchangeRate: { 217 | [Currency.KRW]: krw, 218 | [Currency.JPY]: krwToJyp, 219 | } 220 | }) 221 | 222 | const value = 100000 223 | 224 | const krwPrice = localAirport.fc(value, undefined, Currency.JPY) 225 | const exchangedValue = (value / (krwToJyp / krw)) * (krw / krw) 226 | const koKrKrwIntlFormatted = new Intl.NumberFormat('ko-KR', { style: 'currency', currency: 'KRW' }).format(exchangedValue) 227 | 228 | expect(krwPrice).toBe(koKrKrwIntlFormatted) 229 | }) 230 | 231 | test('airport throws error when keyCurrency is not supported currency.', () => { 232 | expect(() => { 233 | new Airport({ 234 | supportedLocales, 235 | locale: 'ko-KR', 236 | fallbackLocale: 'ko-KR', 237 | currency: { 238 | 'ko-KR': Currency.KRW, 239 | 'en-US': Currency.USD, 240 | 'ja-JP': Currency.JPY, 241 | }, 242 | keyCurrency: 'GBP' 243 | }) 244 | }).toThrow() 245 | }) 246 | 247 | test('airport.fc() can format isFixedCurrency with baseCurrency', () => { 248 | const value = 100000 249 | 250 | const koKrIntlBasedFormatted = airport.fc(value, undefined, Currency.KRW, true) 251 | const koKrKrwIntlFormatted = new Intl.NumberFormat('ko-KR', { style: 'currency', currency: 'KRW' }).format(value) 252 | 253 | expect(koKrIntlBasedFormatted).toBe(koKrKrwIntlFormatted) 254 | 255 | airport.changeLocale('en-US') 256 | 257 | const enUsIntlBasedFormatted = airport.fc(value, undefined, Currency.KRW, true) 258 | const enUsKrwIntlFormatted = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'KRW' }).format(value) 259 | expect(enUsIntlBasedFormatted).toBe(enUsKrwIntlFormatted) 260 | 261 | const formattedCurrencyAirport = new Airport({ 262 | supportedLocales, 263 | locale: 'ko-KR', 264 | fallbackLocale: 'ko-KR', 265 | currency: { 266 | 'ko-KR': Currency.KRW, 267 | 'en-US': Currency.USD, 268 | 'ja-JP': Currency.JPY, 269 | }, 270 | currencyFormat: { 271 | [Currency.KRW]: '{v}원입니다.', 272 | }, 273 | }) 274 | 275 | const koKrLsBasedFormatted = formattedCurrencyAirport.fc(value, undefined, Currency.KRW, true) 276 | const koKrIntlNumberFormatted = new Intl.NumberFormat('ko-KR').format(value) 277 | expect(koKrLsBasedFormatted).toBe(`${koKrIntlNumberFormatted}원입니다.`) 278 | 279 | formattedCurrencyAirport.changeLocale('en-US') 280 | 281 | const enUsLsBasedFormatted = formattedCurrencyAirport.fc(value, undefined, Currency.KRW, true) 282 | const enUsIntlNumberFormatted = new Intl.NumberFormat('en-US').format(value) 283 | expect(enUsLsBasedFormatted).toBe(`${enUsIntlNumberFormatted}원입니다.`) 284 | }) 285 | 286 | }) 287 | -------------------------------------------------------------------------------- /packages/js/src/Airport.ts: -------------------------------------------------------------------------------- 1 | // Airport 2 | // Copyright 2024-present NAVER Corp. 3 | // MIT License 4 | 5 | import { 6 | Currency, 7 | CurrencyMap, 8 | CurrencyType, 9 | ImprovedNumberFormatOptions, 10 | LocaleMap, 11 | LS, 12 | LSO, 13 | Options, 14 | PartialLS, 15 | PartialLSO, 16 | } from './types' 17 | import { createLSFactory, deepEqual, roundOperation } from './utils' 18 | 19 | /** 20 | * **Create Airport instance** 21 | * 22 | * @typeParam `L` - Array of supported locales 23 | * @typeParam `G` - Type of global language set 24 | */ 25 | export class Airport, G extends LS | PartialLS> { 26 | private locale: L[number] 27 | private language: string 28 | private region: string = null 29 | 30 | private fallbackLocale: L[number] 31 | private fallbackLanguage: string 32 | private fallbackRegion: string 33 | 34 | private supportedLocales: L 35 | private globalLS: G 36 | 37 | private currencyMap: LocaleMap, CurrencyType> 38 | private supportedCurrency: CurrencyType[] = [] 39 | private currencyFormatValueKey = 'v' 40 | private currencyFormat: CurrencyMap 41 | private keyCurrency: CurrencyType = Currency.USD 42 | private exchangeRate?: CurrencyMap 43 | 44 | private cachedNumberFormat: Intl.NumberFormat 45 | private cachedNumberFormatOptions: Intl.NumberFormatOptions 46 | 47 | private cachedCurrencyFormat: Intl.NumberFormat 48 | private cachedCurrencyFormatOptions: Intl.NumberFormatOptions 49 | 50 | constructor(private options: Options) { 51 | this.supportedLocales = options.supportedLocales 52 | this.globalLS = (options.globalLS ?? {}) as G 53 | 54 | this.setupFallbackLocale(options.fallbackLocale) 55 | this.setupLocale(options.locale) 56 | this.setupOptions(options) 57 | 58 | this.t = this.t.bind(this) 59 | 60 | this.cachedNumberFormat = new Intl.NumberFormat(options.locale, this.cachedNumberFormatOptions) 61 | this.cachedCurrencyFormat = new Intl.NumberFormat(options.locale, this.cachedCurrencyFormatOptions) 62 | } 63 | 64 | createLS = createLSFactory() 65 | 66 | getOptions = () => { 67 | return { ...this.options } 68 | } 69 | 70 | getLocale = () => { 71 | return this.locale 72 | } 73 | 74 | getLanguage = () => { 75 | return this.language 76 | } 77 | 78 | getRegion = () => { 79 | return this.region 80 | } 81 | 82 | changeLocale = (nextLocale: L[number]) => { 83 | this.setupLocale(nextLocale) 84 | this.cachedNumberFormat = new Intl.NumberFormat(nextLocale, this.cachedNumberFormatOptions) 85 | this.cachedCurrencyFormat = new Intl.NumberFormat(nextLocale, this.cachedCurrencyFormatOptions) 86 | } 87 | 88 | /** 89 | * **Function to translate LSO** 90 | * 91 | * @param lso - Language set object to translate 92 | * For the case of Global LS, pass LS key as string 93 | * @param [variableMap] - Object that includes variable value used in LSO 94 | * @param [_forcedLocale] - Locale to use instead of default locale 95 | */ 96 | t(lso: LSO, variableMap?: any, _forcedLocale?: L[number]): string 97 | t(partialLso: PartialLSO, variableMap?: any, _forcedLocale?: L[number]): string 98 | t(globalLSKey: keyof G, variableMap?: any, _forcedLocale?: L[number]): string 99 | t(stringKey: string, variableMap?: any, _forcedLocale?: L[number]): string 100 | t(lsoOrGlobalLSKey: LSO | PartialLSO | keyof G | string, variableMap?: any, _forcedLocale?: L[number]): string { 101 | let translated = '' 102 | try { 103 | const locale = _forcedLocale ?? this.getLocale() 104 | const language = _forcedLocale ? this.splitLocale(_forcedLocale)[0] : this.getLanguage() 105 | 106 | const variableEntries = Object.entries(variableMap ?? {}) 107 | translated = 108 | typeof lsoOrGlobalLSKey === 'object' 109 | ? lsoOrGlobalLSKey[locale] ?? 110 | lsoOrGlobalLSKey[language as L[number]] ?? 111 | lsoOrGlobalLSKey[this.fallbackLocale] ?? 112 | lsoOrGlobalLSKey[this.fallbackLanguage as L[number]] ?? 113 | '' 114 | : this.globalLS[lsoOrGlobalLSKey]?.[locale as L[number]] ?? 115 | this.globalLS[lsoOrGlobalLSKey]?.[language as L[number]] ?? 116 | (lsoOrGlobalLSKey as string) 117 | 118 | // Insert value 119 | variableEntries.forEach(([key, value]) => { 120 | if (value === undefined) { 121 | translated = translated?.replace(new RegExp(`\\{${key}\\}`, 'gi'), '') 122 | } else { 123 | translated = translated?.replace(new RegExp(`\\{${key}\\}`, 'gi'), value as string) 124 | } 125 | }) 126 | 127 | translated = translated?.replace(new RegExp(`\\{(.[^\\}]*)\\}`, 'gi'), (match, p1) => { 128 | if (Object.keys(variableMap ?? {}).every(variableKey => !p1?.includes(variableKey))) return match 129 | return eval( 130 | `${variableEntries 131 | .map(([key, value]) => { 132 | let val = value 133 | if (typeof val === 'string') { 134 | val = val.replace(/['"]+/g, '') 135 | } 136 | return `var ${key} = ${typeof value === 'string' ? `'${val}'` : value};` 137 | }) 138 | .join('')}${p1};`, 139 | ) 140 | }) 141 | } catch (e) { 142 | console.error(e) 143 | } finally { 144 | return translated 145 | } 146 | } 147 | 148 | /** 149 | * **Function to format the given number and return** 150 | * 151 | * @param value - Number to format 152 | * @param [options] - Formatting option to apply. Follows Intl.NumberFormatOptions format 153 | * @param [_forcedLocale] - Locale to use instead of default locale 154 | */ 155 | fn = (value: number, options?: ImprovedNumberFormatOptions, _forcedLocale?: L[number]) => { 156 | const numberFormat = this.getNumberFormatInstance(options, _forcedLocale) 157 | 158 | if (options?.roundingMode) { 159 | const adjustedNumber = Number(numberFormat.format(value).replace(/[^0-9.\-]/g, '')) 160 | const roundingModeNumber = roundOperation(value, adjustedNumber, options.roundingMode) 161 | return numberFormat.format(roundingModeNumber) 162 | } 163 | 164 | return numberFormat.format(value) 165 | } 166 | 167 | /** 168 | * **Function to convert and format the currency of given value and return** 169 | * 170 | * @param value - Number to format 171 | * @param [customFormat] - Currency Format to apply (default: currencyFormat provided in constructor) 172 | * @param [baseCurrency] - Currency of `value`. 173 | * `baseCurrency` is required if `isFixedCurrency` is `true`. 174 | * @param [isFixedCurrency] - If `isFixedCurrency` is true, `value` will not be exchanged and formatted to current locale's currency. 175 | * @param [_forcedLocale] - Locale to use instead of default locale 176 | */ 177 | fc = ( 178 | value: number, 179 | customFormat?: string, 180 | baseCurrency?: Currency, 181 | isFixedCurrency = false, 182 | _forcedLocale?: L[number], 183 | ) => { 184 | if (!this.currencyMap) { 185 | console.error('You need to set "currency" options for using fc()') 186 | return 187 | } 188 | 189 | const locale = _forcedLocale ?? this.getLocale() 190 | 191 | if (isFixedCurrency) { 192 | if (!baseCurrency) { 193 | console.error('You cannot fix currency without baseCurrency.') 194 | return 195 | } 196 | return this.formatCurrency(value, baseCurrency, customFormat, _forcedLocale) 197 | } 198 | 199 | const currentCurrency = this.currencyMap[locale] 200 | if (!baseCurrency || baseCurrency === currentCurrency) { 201 | return this.formatCurrency(value, currentCurrency, customFormat, _forcedLocale) 202 | } 203 | 204 | const keyCurrencyER = this.exchangeRate[this.keyCurrency] 205 | const startCurrencyER = this.exchangeRate[baseCurrency] / keyCurrencyER 206 | const endCurrencyER = this.exchangeRate[currentCurrency] / keyCurrencyER 207 | 208 | if (!startCurrencyER || !endCurrencyER) { 209 | return this.formatCurrency(value, baseCurrency, customFormat, _forcedLocale) 210 | } 211 | 212 | const exchangedValue = (value / startCurrencyER) * endCurrencyER 213 | 214 | return this.formatCurrency(exchangedValue, currentCurrency, customFormat, _forcedLocale) 215 | } 216 | 217 | private formatCurrency( 218 | value: number, 219 | targetCurrency: CurrencyType, 220 | customFormat?: string, 221 | _forcedLocale?: L[number], 222 | ) { 223 | let format 224 | if (customFormat) format = customFormat 225 | else if (this.currencyFormat?.[targetCurrency]) format = this.currencyFormat?.[targetCurrency] 226 | 227 | return format 228 | ? this.t(format, { [this.currencyFormatValueKey]: this.fn(value) }) 229 | : this.getCurrencyFormatInstance({ style: 'currency', currency: targetCurrency }, _forcedLocale).format(value) 230 | } 231 | 232 | private setupLocale(locale: L[number]) { 233 | if (!locale) { 234 | console.error('There is no input locale.') 235 | return 236 | } 237 | 238 | const [language, region] = this.splitLocale(locale) 239 | 240 | if (!this.isSupportedLocale(locale, language)) { 241 | this.useFallbackLocale() 242 | return 243 | } 244 | 245 | this.applyLocale(locale, language, region) 246 | } 247 | 248 | private setupFallbackLocale(fallbackLocale: L[number]) { 249 | if (!this.isSupportedLocale(fallbackLocale)) { 250 | throw new Error('options.fallbackLocale must be value in the options.supportedLocales') 251 | } 252 | 253 | this.fallbackLocale = fallbackLocale 254 | 255 | const [language, region] = this.splitLocale(fallbackLocale) 256 | this.fallbackLanguage = language 257 | this.fallbackRegion = region 258 | } 259 | 260 | private applyLocale(locale: L[number], language: string, region: string) { 261 | this.locale = locale 262 | this.language = language 263 | this.region = region 264 | } 265 | 266 | // Function to renew and return cached instance of NumberFormat. 267 | private getNumberFormatInstance(options?: ImprovedNumberFormatOptions, _forcedLocale?: L[number]) { 268 | if (_forcedLocale) return new Intl.NumberFormat(_forcedLocale, options) 269 | if (!deepEqual(this.cachedNumberFormatOptions, options)) { 270 | this.cachedNumberFormatOptions = options 271 | this.cachedNumberFormat = new Intl.NumberFormat(this.getLocale(), options) 272 | } 273 | 274 | return this.cachedNumberFormat 275 | } 276 | 277 | // Function to renew and return cached instance of CurrencyNumberFormat. 278 | private getCurrencyFormatInstance(options?: ImprovedNumberFormatOptions, _forcedLocale?: L[number]) { 279 | if (_forcedLocale) return new Intl.NumberFormat(_forcedLocale, options) 280 | if (!deepEqual(this.cachedCurrencyFormatOptions, options)) { 281 | this.cachedCurrencyFormatOptions = options 282 | this.cachedCurrencyFormat = new Intl.NumberFormat(this.getLocale(), options) 283 | } 284 | 285 | return this.cachedCurrencyFormat 286 | } 287 | 288 | private useFallbackLocale() { 289 | this.applyLocale(this.fallbackLocale, this.fallbackLanguage, this.fallbackRegion) 290 | } 291 | 292 | private splitLocale(locale: L[number]) { 293 | return locale.split('-') 294 | } 295 | 296 | private isSupportedLocale(locale: L[number], language?: string) { 297 | return this.supportedLocales.includes(locale) || (language && this.supportedLocales.includes(language)) 298 | } 299 | 300 | private setupOptions(options: Options) { 301 | this.setupCurrency(options) 302 | } 303 | 304 | private setupCurrency(options: Options) { 305 | if (this.currencyMap || !options.currency) return 306 | 307 | this.currencyMap = { 308 | ...options.currency, 309 | } as LocaleMap, Currency> 310 | 311 | this.supportedCurrency = [...Object.values(this.currencyMap)] as Currency[] 312 | 313 | if (options.keyCurrency) { 314 | this.keyCurrency = options.keyCurrency 315 | } 316 | 317 | if (!this.supportedCurrency.includes(this.keyCurrency)) { 318 | throw new Error('keyCurrency must be included on the options.currency.') 319 | } 320 | 321 | if (options.currencyFormatValueKey) { 322 | this.currencyFormatValueKey = options.currencyFormatValueKey 323 | } 324 | 325 | if (options.currencyFormat) { 326 | this.currencyFormat = { 327 | ...options.currencyFormat, 328 | } 329 | } 330 | 331 | if (options.exchangeRate) { 332 | this.exchangeRate = { 333 | ...options.exchangeRate, 334 | } 335 | 336 | this.exchangeRate[this.keyCurrency] ??= 1 337 | } 338 | } 339 | } 340 | --------------------------------------------------------------------------------