├── 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 |
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 | 
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 | 
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 | 
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 | 
22 |
23 |
24 | 
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 | 
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 | 
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 | 
19 | >Refresh(Reset) button fetches lastest LS of target branch
20 |
21 | 
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 | 
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 | 
11 |
12 |
13 | You can check modified items by clicking on the LS id in `Local Changes` in the left partition.
14 |
15 |
16 | 
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 | 
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 | 
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 |
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 {t(testLS.buttonTitle)}
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 {t(testLS.navTitle, { button: })}
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 |
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 | 
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 |
--------------------------------------------------------------------------------