├── .husky ├── .gitignore ├── pre-commit └── commit-msg ├── .eslintignore ├── .gitignore ├── commitlint.config.js ├── tsconfig.eslint.json ├── src ├── index.ts ├── formatAddress.ts └── addressFormats.ts ├── jest.config.js ├── .eslintrc.json ├── tsconfig.json ├── rollup.config.js ├── release.config.js ├── .github └── workflows │ ├── test.yml │ ├── release.yml │ └── update-formats.yml ├── LICENSE ├── package.json ├── CHANGELOG.md ├── README.md ├── bin └── update-formats.ts └── test └── formatAddress.test.ts /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | *.js 2 | /dist 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /dist 3 | /coverage 4 | /.eslintcache 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = {extends: ['@commitlint/config-conventional']}; 2 | -------------------------------------------------------------------------------- /tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["./"] 4 | } 5 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no -- commitlint --edit "" 5 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export type {Address} from './formatAddress'; 2 | export {default as formatAddress} from './formatAddress'; 3 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | }; 5 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-dasprid", 3 | "parserOptions": { 4 | "project": "tsconfig.eslint.json" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/recommended/tsconfig.json", 3 | "compilerOptions": { 4 | "noImplicitAny": true, 5 | "strictNullChecks": true, 6 | "importsNotUsedAsValues": "error", 7 | "declaration": true, 8 | "sourceMap": true, 9 | "outDir": "dist", 10 | "declarationDir": "." 11 | }, 12 | "include": ["src/**/*"], 13 | "exclude": ["dist"] 14 | } 15 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from '@rollup/plugin-typescript'; 2 | import pkg from './package.json'; 3 | 4 | export default [ 5 | { 6 | input: 'src/index.ts', 7 | output: [ 8 | {name: 'localizedAddressFormat', file: pkg.browser, format: 'umd', sourcemap: true}, 9 | {file: pkg.main, format: 'cjs', sourcemap: true}, 10 | {file: pkg.module, format: 'es', sourcemap: true}, 11 | ], 12 | plugins: [ 13 | typescript({tsconfig: './tsconfig.json'}), 14 | ], 15 | }, 16 | ]; 17 | -------------------------------------------------------------------------------- /release.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | branches: [ 3 | 'main', 4 | ], 5 | plugins: [ 6 | '@semantic-release/commit-analyzer', 7 | '@semantic-release/release-notes-generator', 8 | [ 9 | '@semantic-release/changelog', 10 | { 11 | changelogFile: 'CHANGELOG.md', 12 | changelogTitle: '# Changelog', 13 | }, 14 | ], 15 | '@semantic-release/npm', 16 | '@semantic-release/github', 17 | [ 18 | '@semantic-release/git', 19 | { 20 | message: 'chore(release): set `package.json` to ${nextRelease.version} [skip ci]' 21 | + '\n\n${nextRelease.notes}', 22 | }, 23 | ], 24 | ], 25 | }; 26 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | test: 10 | name: Test 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Use Node.js 24.x 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version: 24.x 20 | 21 | - name: Cache node modules 22 | uses: actions/cache@v4 23 | env: 24 | cache-name: cache-node-modules 25 | with: 26 | path: ~/.npm 27 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} 28 | restore-keys: | 29 | ${{ runner.os }}-build-${{ env.cache-name }}- 30 | ${{ runner.os }}-build- 31 | ${{ runner.os }}- 32 | 33 | - name: Install Dependencies 34 | run: npm ci 35 | 36 | - name: Lint 37 | run: npm run lint 38 | 39 | - name: Test 40 | run: npm test 41 | 42 | - name: Codecov 43 | uses: codecov/codecov-action@v2 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2020-2022, Ben Scholzen 'DASPRiD' 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a 4 | copy of this software and associated documentation files (the "Software"), 5 | to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | and/or sell copies of the Software, and to permit persons to whom the 8 | Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 16 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | permissions: 9 | id-token: write 10 | contents: write 11 | issues: write 12 | 13 | jobs: 14 | release: 15 | name: Release 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | 21 | - name: Use Node.js 24.x 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: 24.x 25 | registry-url: 'https://registry.npmjs.org' 26 | 27 | - name: Cache node modules 28 | uses: actions/cache@v4 29 | env: 30 | cache-name: cache-node-modules 31 | with: 32 | path: ~/.npm 33 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} 34 | restore-keys: | 35 | ${{ runner.os }}-build-${{ env.cache-name }}- 36 | ${{ runner.os }}-build- 37 | ${{ runner.os }}- 38 | 39 | - name: Install Dependencies 40 | run: npm ci 41 | 42 | - name: Lint 43 | run: npm run lint 44 | 45 | - name: Test 46 | run: npm test 47 | 48 | - name: Codecov 49 | uses: codecov/codecov-action@v2 50 | 51 | - name: Build 52 | run: npm run build 53 | 54 | - name: Install semantic-release extra plugins 55 | run: npm install --no-save @semantic-release/changelog@^6 @semantic-release/git@^10 56 | 57 | - name: Release 58 | env: 59 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 60 | HUSKY: 0 61 | run: npx semantic-release@^25 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "localized-address-format", 3 | "version": "1.3.3", 4 | "description": "Localized Address Formatting", 5 | "main": "dist/index.cjs.js", 6 | "module": "dist/index.esm.js", 7 | "browser": "dist/index.umd.js", 8 | "types": "dist/index.d.ts", 9 | "scripts": { 10 | "update-formats": "ts-node bin/update-formats.ts", 11 | "build": "rm -rf dist && rollup -c", 12 | "lint": "eslint .", 13 | "test": "jest test --coverage", 14 | "prepare": "husky install" 15 | }, 16 | "homepage": "https://github.com/DASPRiD/localized-address-format", 17 | "bugs": { 18 | "url": "https://github.com/DASPRiD/localized-address-format/issues" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/DASPRiD/localized-address-format.git" 23 | }, 24 | "files": [ 25 | "dist/*" 26 | ], 27 | "keywords": [ 28 | "address", 29 | "formatting", 30 | "l10n", 31 | "localize", 32 | "typescript" 33 | ], 34 | "author": "Ben Scholzen 'DASPRiD'", 35 | "license": "MIT", 36 | "devDependencies": { 37 | "@commitlint/cli": "^16.2.3", 38 | "@commitlint/config-conventional": "^16.2.1", 39 | "@rollup/plugin-typescript": "^8.3.1", 40 | "@tsconfig/recommended": "^1.0.1", 41 | "@types/cli-progress": "^3.4.2", 42 | "@types/jest": "^27.0.2", 43 | "@types/node": "^16.9.6", 44 | "@types/node-fetch": "^2.5.12", 45 | "cli-progress": "^3.6.1", 46 | "eslint": "^8.12.0", 47 | "eslint-config-dasprid": "^0.1.12", 48 | "husky": "^7.0.4", 49 | "jest": "^27.2.1", 50 | "lint-staged": "^12.3.7", 51 | "node-fetch": "^2.6.5", 52 | "rollup": "^2.70.1", 53 | "ts-jest": "^27.1.4", 54 | "ts-node": "^10.2.1", 55 | "typescript": "^4.4.3" 56 | }, 57 | "lint-staged": { 58 | "*.js": "eslint --cache --fix" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [1.3.3](https://github.com/DASPRiD/localized-address-format/compare/v1.3.2...v1.3.3) (2025-10-23) 4 | 5 | 6 | ### Bug Fixes 7 | 8 | * add provenance attestation ([a6d9abd](https://github.com/DASPRiD/localized-address-format/commit/a6d9abdf793efeba1df93c74fe340a7bc37a69d1)) 9 | 10 | ## [1.3.2](https://github.com/DASPRiD/localized-address-format/compare/v1.3.1...v1.3.2) (2025-08-15) 11 | 12 | 13 | ### Bug Fixes 14 | 15 | * **formats:** update formats ([95fd9fd](https://github.com/DASPRiD/localized-address-format/commit/95fd9fdae0dfe544b377ac594943b0b8f702a0ed)) 16 | 17 | ## [1.3.1](https://github.com/DASPRiD/localized-address-format/compare/v1.3.0...v1.3.1) (2023-03-26) 18 | 19 | 20 | ### Bug Fixes 21 | 22 | * **formats:** update address formats to 2023-03-26 ([7b1a0f1](https://github.com/DASPRiD/localized-address-format/commit/7b1a0f1e17c581b489ccf62fbdd0194a825798dc)) 23 | 24 | # [1.3.0](https://github.com/DASPRiD/localized-address-format/compare/v1.2.1...v1.3.0) (2022-12-13) 25 | 26 | 27 | ### Features 28 | 29 | * allow switching to latin script type ([4f9154f](https://github.com/DASPRiD/localized-address-format/commit/4f9154f165c736270e88cac2567122ee213a1a96)), closes [#16](https://github.com/DASPRiD/localized-address-format/issues/16) 30 | 31 | ## [1.2.1](https://github.com/DASPRiD/localized-address-format/compare/v1.2.0...v1.2.1) (2022-03-26) 32 | 33 | 34 | ### Bug Fixes 35 | 36 | * **formats:** update address formats to 2022-03-26 ([04dab31](https://github.com/DASPRiD/localized-address-format/commit/04dab313da12b964e34776a6c9d015564785eefd)) 37 | 38 | ## [1.2.0](https://github.com/DASPRiD/localized-address-format/compare/v1.1.0...v1.2.0) (2022-03-26) 39 | 40 | 41 | ### Features 42 | 43 | * rewrite library, update address formats, use newer dev dependencies and utilize GitHub actions ([81f482e](https://github.com/DASPRiD/localized-address-format/commit/81f482e0504049a4de9459f05946ad9add5c57e9)) 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Localized Address Formatting 2 | 3 | [![npm version](https://badge.fury.io/js/localized-address-format.svg)](https://badge.fury.io/js/localized-address-format) 4 | [![Release](https://github.com/DASPRiD/localized-address-format/actions/workflows/release.yml/badge.svg)](https://github.com/DASPRiD/localized-address-format/actions/workflows/release.yml) 5 | [![codecov](https://codecov.io/gh/DASPRiD/localized-address-format/branch/main/graph/badge.svg?token=ID1YAAB9CP)](https://codecov.io/gh/DASPRiD/localized-address-format) 6 | 7 | 8 | Zero-dependency address formatting library. 9 | 10 | If you are in need for simply formatting postal addresses in a localized format, this is the right library for you. It 11 | is based on code from [libaddressinput](https://github.com/google/libaddressinput) by Google and uses the same data from 12 | their [Address Data Service](https://chromium-i18n.appspot.com/ssl-address/data), but in a pruned format which is only 13 | a little over 5kb in size. 14 | 15 | Usage of the library is very straight forward: 16 | 17 | ```typescript 18 | import {formatAddress} from 'localized-address-format'; 19 | 20 | console.log(formatAddress({ 21 | postalCountry: 'US', 22 | administrativeArea : 'CA', 23 | locality: 'San Fransisco', 24 | //dependentLocality: '', 25 | postalCode: '94016', 26 | //sortingCode: '', 27 | organization: 'Example Org.', 28 | name: 'Jon Doe', 29 | addressLines : ['548 Market St'], 30 | }).join('\n')); 31 | ``` 32 | 33 | This library formats addresses in the local script type by default. If you prefer to use the latin script type, pass 34 | `latin` as second argument to the `formatAddress()` function. 35 | 36 | All fields of the address are optional and can either be left out completely or passed in as empty string or, in the 37 | case of `addressLines`, be an empty array. The format of the resulting address is dependent on the `postalCountry`. 38 | 39 | The formatted address will not include the country itself. Adding the country to the end of the address is up to your 40 | application. 41 | -------------------------------------------------------------------------------- /.github/workflows/update-formats.yml: -------------------------------------------------------------------------------- 1 | name: Update formats 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: '0 20 * * 0' 7 | 8 | jobs: 9 | updateFormats: 10 | name: Update formats 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Use Node.js 24.x 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version: 24.x 20 | 21 | - name: Cache node modules 22 | uses: actions/cache@v4 23 | env: 24 | cache-name: cache-node-modules 25 | with: 26 | path: ~/.npm 27 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} 28 | restore-keys: | 29 | ${{ runner.os }}-build-${{ env.cache-name }}- 30 | ${{ runner.os }}-build- 31 | ${{ runner.os }}- 32 | 33 | - name: Install Dependencies 34 | run: npm ci 35 | 36 | - name: Update formats 37 | run: npm run update-formats 38 | 39 | - name: Get date 40 | id: get-date 41 | run: echo "::set-output name=DATE::$(/bin/date -u "+%F")" 42 | 43 | - uses: tibdex/github-app-token@v1 44 | id: generate-token 45 | with: 46 | app_id: ${{ secrets.APP_ID }} 47 | private_key: ${{ secrets.APP_PRIVATE_KEY }} 48 | 49 | - name: Create pull request 50 | uses: peter-evans/create-pull-request@v4 51 | with: 52 | token: ${{ steps.generate-token.outputs.token }} 53 | base: ${{ steps.branches.outputs.BASE }} 54 | branch: feature/scheduled-formats-update 55 | delete-branch: true 56 | commit-message: 'fix(formats): update address formats to ${{ steps.get-date.outputs.DATE }}' 57 | title: Update address formats to ${{ steps.get-date.outputs.DATE }} 58 | body: | 59 | Formats updated to latest state from ${{ steps.get-date.outputs.DATE }}. 60 | 61 | This PR is auto-generated by [create-pull-request](https://github.com/peter-evans/create-pull-request) 62 | using the `.github/workflows/update-formats.yml` workflow. 63 | labels: | 64 | Type: enhancement 65 | reviewers: dasprid 66 | -------------------------------------------------------------------------------- /bin/update-formats.ts: -------------------------------------------------------------------------------- 1 | import {promises as fs} from 'fs'; 2 | import {SingleBar} from 'cli-progress'; 3 | import fetch from 'node-fetch'; 4 | 5 | const serviceUrl = 'https://chromium-i18n.appspot.com/ssl-address/data'; 6 | 7 | type RootData = { 8 | id : 'data'; 9 | countries : string; 10 | }; 11 | 12 | const getCountryCodes = async () : Promise => { 13 | const response = await fetch(serviceUrl); 14 | 15 | if (!response.ok) { 16 | throw new Error('Could not load country list'); 17 | } 18 | 19 | const data = await response.json() as RootData; 20 | return [...data.countries.split('~'), 'ZZ']; 21 | }; 22 | 23 | type CountryData = { 24 | key ?: string; 25 | fmt ?: string; 26 | lfmt ?: string; 27 | }; 28 | 29 | const getCountryData = async (countryCode : string, progressBar : SingleBar) : Promise => { 30 | for (let i = 0; i < 5; ++i) { 31 | try { 32 | const response = await fetch(`${serviceUrl}/${countryCode}`); 33 | 34 | if (!response.ok) { 35 | continue; 36 | } 37 | 38 | const data = await response.json() as CountryData; 39 | 40 | progressBar.increment(); 41 | return data; 42 | } catch { 43 | // Retry up to 5 times. 44 | } 45 | } 46 | 47 | throw new Error('Could not load data'); 48 | }; 49 | 50 | const main = async () => { 51 | const countryCodes = await getCountryCodes(); 52 | const promises = []; 53 | 54 | const progressBar = new SingleBar({hideCursor: true}); 55 | progressBar.start(countryCodes.length, 0); 56 | 57 | for (const countryCode of countryCodes) { 58 | promises.push(getCountryData(countryCode, progressBar)); 59 | } 60 | 61 | const countries = await Promise.all(promises); 62 | progressBar.stop(); 63 | const lines = [ 64 | '// This file is auto-generated via "npm run update-formats". Do not alter manually!', 65 | '', 66 | 'type Format = {', 67 | ' local : string;', 68 | ' latin ?: string;', 69 | '};', 70 | '', 71 | 'const addressFormats = new Map([', 72 | ]; 73 | let defaultAddressFormat : string | null = null; 74 | 75 | for (const country of countries) { 76 | if (!country.fmt) { 77 | continue; 78 | } 79 | 80 | if (!country.key) { 81 | defaultAddressFormat = country.fmt; 82 | continue; 83 | } 84 | 85 | const formats = [`local: '${country.fmt}'`]; 86 | 87 | if (country.lfmt) { 88 | formats.push(`latin: '${country.lfmt}'`); 89 | } 90 | 91 | lines.push(` ['${country.key}', {${formats.join(', ')}}],`); 92 | } 93 | 94 | lines.push(']);'); 95 | 96 | if (!defaultAddressFormat) { 97 | throw new Error('Default address format missing'); 98 | } 99 | 100 | lines.push( 101 | '', 102 | `export const defaultAddressFormat = '${defaultAddressFormat}';`, 103 | '', 104 | 'export default addressFormats;', 105 | '', 106 | ); 107 | 108 | await fs.writeFile(`${__dirname}/../src/addressFormats.ts`, lines.join('\n')); 109 | }; 110 | 111 | main().catch(error => { 112 | console.error(error instanceof Error ? error.message : error); 113 | process.exit(1); 114 | }); 115 | -------------------------------------------------------------------------------- /test/formatAddress.test.ts: -------------------------------------------------------------------------------- 1 | import {formatAddress} from '../src'; 2 | 3 | describe('formatAddress', () => { 4 | it('should omit missing fields with literals between fields', () => { 5 | expect(formatAddress( 6 | {postalCountry: 'US'} 7 | )).toStrictEqual([]); 8 | expect(formatAddress( 9 | {postalCountry: 'US', administrativeArea: 'CA'} 10 | )).toStrictEqual(['CA']); 11 | expect(formatAddress( 12 | {postalCountry: 'US', administrativeArea: 'CA', locality: 'Los Angeles'} 13 | )).toStrictEqual(['Los Angeles, CA']); 14 | expect(formatAddress( 15 | {postalCountry: 'US', administrativeArea: 'CA', locality: 'Los Angeles', postalCode: '90291'} 16 | )).toStrictEqual(['Los Angeles, CA 90291']); 17 | expect(formatAddress( 18 | {postalCountry: 'US', locality: 'Los Angeles', postalCode: '90291'} 19 | )).toStrictEqual(['Los Angeles 90291']); 20 | expect(formatAddress( 21 | {postalCountry: 'US', administrativeArea: 'CA', postalCode: '90291'} 22 | )).toStrictEqual(['CA 90291']); 23 | }); 24 | 25 | it('should omit missing fields with literals on separate lines', () => { 26 | expect(formatAddress( 27 | {postalCountry: 'AX'} 28 | )).toStrictEqual(['ÅLAND']); 29 | expect(formatAddress( 30 | {postalCountry: 'AX', locality: 'City'} 31 | )).toStrictEqual(['City', 'ÅLAND']); 32 | expect(formatAddress( 33 | {postalCountry: 'AX', locality: 'City', postalCode: '123'} 34 | )).toStrictEqual(['AX-123 City', 'ÅLAND']); 35 | }); 36 | 37 | it('should remove empty address lines', () => { 38 | expect(formatAddress( 39 | {postalCountry: 'US', addressLines: ['foo', '', 'bar']} 40 | )).toStrictEqual(['foo', 'bar']); 41 | expect(formatAddress( 42 | {postalCountry: 'US', addressLines: ['']} 43 | )).toStrictEqual([]); 44 | expect(formatAddress( 45 | {postalCountry: 'US', addressLines: []} 46 | )).toStrictEqual([]); 47 | }); 48 | 49 | it('should fall back to default format string', () => { 50 | expect(formatAddress( 51 | {postalCountry: 'ZZ', name: 'Name', organization: 'Org', addressLines: ['Line 1'], locality: 'City'} 52 | )).toStrictEqual(['Name', 'Org', 'Line 1', 'City']); 53 | expect(formatAddress( 54 | {name: 'Name', organization: 'Org', addressLines: ['Line 1'], locality: 'City'} 55 | )).toStrictEqual(['Name', 'Org', 'Line 1', 'City']); 56 | }); 57 | 58 | it('should use local script type by default', () => { 59 | expect(formatAddress( 60 | {postalCountry: 'HK', name: 'Name', organization: 'Org', addressLines: ['Line 1'], locality: 'City'} 61 | )).toStrictEqual(['City', 'Line 1', 'Org', 'Name']); 62 | }); 63 | 64 | it('should use latin script type when requested', () => { 65 | expect(formatAddress( 66 | {postalCountry: 'HK', name: 'Name', organization: 'Org', addressLines: ['Line 1'], locality: 'City'}, 67 | 'latin' 68 | )).toStrictEqual(['Name', 'Org', 'Line 1', 'City']); 69 | }); 70 | 71 | it('should fall back to local script type when latin is not available', () => { 72 | expect(formatAddress( 73 | {postalCountry: 'US', name: 'Name', organization: 'Org', addressLines: ['Line 1'], locality: 'City'}, 74 | 'latin' 75 | )).toStrictEqual(['Name', 'Org', 'Line 1', 'City']); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /src/formatAddress.ts: -------------------------------------------------------------------------------- 1 | import addressFormats, {defaultAddressFormat} from './addressFormats'; 2 | 3 | export type ScriptType = 'local' | 'latin'; 4 | 5 | export type Address = { 6 | postalCountry ?: string; 7 | administrativeArea ?: string; 8 | locality ?: string; 9 | dependentLocality ?: string; 10 | postalCode ?: string; 11 | sortingCode ?: string; 12 | organization ?: string; 13 | name ?: string; 14 | addressLines ?: string[]; 15 | }; 16 | 17 | type AddressField = keyof Address; 18 | 19 | const getFormatString = (countryCode : string, scriptType : ScriptType) : string => { 20 | const format = addressFormats.get(countryCode.toUpperCase()); 21 | 22 | if (!format) { 23 | return defaultAddressFormat; 24 | } 25 | 26 | return format[scriptType] ?? format.local; 27 | }; 28 | 29 | const getFormatSubstrings = (format : string) : string[] => { 30 | const parts : string[] = []; 31 | let escaped = false; 32 | let currentLiteral = ''; 33 | 34 | for (const char of format) { 35 | if (escaped) { 36 | escaped = false; 37 | parts.push(`%${char}`); 38 | continue; 39 | } 40 | 41 | if (char !== '%') { 42 | currentLiteral += char; 43 | continue; 44 | } 45 | 46 | if (currentLiteral.length > 0) { 47 | parts.push(currentLiteral); 48 | currentLiteral = ''; 49 | } 50 | 51 | escaped = true; 52 | } 53 | 54 | if (currentLiteral.length > 0) { 55 | parts.push(currentLiteral); 56 | } 57 | 58 | return parts; 59 | }; 60 | 61 | const fields = new Map([ 62 | ['%N', 'name'], 63 | ['%O', 'organization'], 64 | ['%A', 'addressLines'], 65 | ['%D', 'dependentLocality'], 66 | ['%C', 'locality'], 67 | ['%S', 'administrativeArea'], 68 | ['%Z', 'postalCode'], 69 | ['%X', 'sortingCode'], 70 | ['%R', 'postalCountry'], 71 | ]); 72 | 73 | const getFieldForFormatSubstring = (formatSubstring : string) : AddressField => { 74 | const field = fields.get(formatSubstring); 75 | 76 | /* istanbul ignore next imported format strings should never contain invalid substrings */ 77 | if (!field) { 78 | throw new Error(`Could not find field for format substring ${formatSubstring}`); 79 | } 80 | 81 | return field; 82 | }; 83 | 84 | const addressHasValueForField = (address : Address, field : AddressField) : boolean => { 85 | if (field === 'addressLines') { 86 | return address.addressLines !== undefined && address.addressLines.length > 0; 87 | } 88 | 89 | return address[field] !== undefined && address[field] !== ''; 90 | }; 91 | 92 | const formatSubstringRepresentsField = (formatSubstring : string) : boolean => { 93 | return formatSubstring !== '%n' && formatSubstring.startsWith('%'); 94 | }; 95 | 96 | const pruneFormat = (formatSubstrings : string[], address : Address) : string[] => { 97 | const prunedFormat : string[] = []; 98 | 99 | for (const [i, formatSubstring] of formatSubstrings.entries()) { 100 | // Always keep the newlines. 101 | if (formatSubstring === '%n') { 102 | prunedFormat.push(formatSubstring); 103 | continue; 104 | } 105 | 106 | if (formatSubstringRepresentsField(formatSubstring)) { 107 | // Always keep non-empty address fields. 108 | if (addressHasValueForField(address, getFieldForFormatSubstring(formatSubstring))) { 109 | prunedFormat.push(formatSubstring); 110 | } 111 | 112 | continue; 113 | } 114 | 115 | // Only keep literals that satisfy these two conditions: 116 | // 1. Not preceding an empty field. 117 | // 2. Not following a removed field. 118 | if (( 119 | i === formatSubstrings.length - 1 120 | || formatSubstrings[i + 1] === '%n' 121 | || addressHasValueForField(address, getFieldForFormatSubstring(formatSubstrings[i + 1])) 122 | ) && ( 123 | i === 0 124 | || !formatSubstringRepresentsField(formatSubstrings[i - 1]) 125 | || (prunedFormat.length > 0 && formatSubstringRepresentsField(prunedFormat[prunedFormat.length - 1])) 126 | )) { 127 | prunedFormat.push(formatSubstring); 128 | } 129 | } 130 | 131 | return prunedFormat; 132 | }; 133 | 134 | const formatAddress = (address : Address, scriptType : ScriptType = 'local') : string[] => { 135 | const formatString = getFormatString(address.postalCountry ?? 'ZZ', scriptType); 136 | const formatSubstrings = getFormatSubstrings(formatString); 137 | const prunedFormat = pruneFormat(formatSubstrings, address); 138 | 139 | const lines : string[] = []; 140 | let currentLine = ''; 141 | 142 | for (const formatSubstring of prunedFormat) { 143 | if (formatSubstring === '%n') { 144 | if (currentLine.length > 0) { 145 | lines.push(currentLine); 146 | currentLine = ''; 147 | } 148 | 149 | continue; 150 | } 151 | 152 | if (!formatSubstringRepresentsField(formatSubstring)) { 153 | // Not a symbol we recognize, so must be a literal. We append it unchanged. 154 | currentLine += formatSubstring; 155 | continue; 156 | } 157 | 158 | const field = getFieldForFormatSubstring(formatSubstring); 159 | 160 | /* istanbul ignore next imported format strings should never contain the postal country */ 161 | if (field === 'postalCountry') { 162 | // Country name is treated separately. 163 | continue; 164 | } 165 | 166 | if (field === 'addressLines') { 167 | // The field "address lines" represents the address lines of an address, so there can be multiple values. 168 | // It is safe to assert addressLines to be defined here, as the pruning process already checked for that. 169 | const addressLines = (address.addressLines as string[]).filter(addressLine => addressLine !== ''); 170 | 171 | if (addressLines.length === 0) { 172 | // Empty address lines are ignored. 173 | continue; 174 | } 175 | 176 | currentLine += addressLines[0]; 177 | 178 | if (addressLines.length > 1) { 179 | lines.push(currentLine); 180 | currentLine = ''; 181 | lines.push(...addressLines.slice(1)); 182 | } 183 | 184 | continue; 185 | } 186 | 187 | // Any other field can be appended as is. 188 | currentLine += address[field]; 189 | } 190 | 191 | if (currentLine.length > 0) { 192 | lines.push(currentLine); 193 | } 194 | 195 | return lines; 196 | }; 197 | 198 | export default formatAddress; 199 | -------------------------------------------------------------------------------- /src/addressFormats.ts: -------------------------------------------------------------------------------- 1 | // This file is auto-generated via "npm run update-formats". Do not alter manually! 2 | 3 | type Format = { 4 | local : string; 5 | latin ?: string; 6 | }; 7 | 8 | const addressFormats = new Map([ 9 | ['AC', {local: '%N%n%O%n%A%n%C%n%Z'}], 10 | ['AD', {local: '%N%n%O%n%A%n%Z %C'}], 11 | ['AE', {local: '%N%n%O%n%A%n%S', latin: '%N%n%O%n%A%n%S'}], 12 | ['AF', {local: '%N%n%O%n%A%n%C%n%Z'}], 13 | ['AI', {local: '%N%n%O%n%A%n%C%n%Z'}], 14 | ['AL', {local: '%N%n%O%n%A%n%Z%n%C'}], 15 | ['AM', {local: '%N%n%O%n%A%n%Z%n%C%n%S', latin: '%N%n%O%n%A%n%Z%n%C%n%S'}], 16 | ['AR', {local: '%N%n%O%n%A%n%Z %C%n%S'}], 17 | ['AS', {local: '%N%n%O%n%A%n%C %S %Z'}], 18 | ['AT', {local: '%O%n%N%n%A%n%Z %C'}], 19 | ['AU', {local: '%O%n%N%n%A%n%C %S %Z'}], 20 | ['AX', {local: '%O%n%N%n%A%nAX-%Z %C%nÅLAND'}], 21 | ['AZ', {local: '%N%n%O%n%A%nAZ %Z %C'}], 22 | ['BA', {local: '%N%n%O%n%A%n%Z %C'}], 23 | ['BB', {local: '%N%n%O%n%A%n%C, %S %Z'}], 24 | ['BD', {local: '%N%n%O%n%A%n%C - %Z'}], 25 | ['BE', {local: '%O%n%N%n%A%n%Z %C'}], 26 | ['BF', {local: '%N%n%O%n%A%n%C %X'}], 27 | ['BG', {local: '%N%n%O%n%A%n%Z %C'}], 28 | ['BH', {local: '%N%n%O%n%A%n%C %Z'}], 29 | ['BL', {local: '%O%n%N%n%A%n%Z %C %X'}], 30 | ['BM', {local: '%N%n%O%n%A%n%C %Z'}], 31 | ['BN', {local: '%N%n%O%n%A%n%C %Z'}], 32 | ['BR', {local: '%O%n%N%n%A%n%D%n%C-%S%n%Z'}], 33 | ['BS', {local: '%N%n%O%n%A%n%C, %S'}], 34 | ['BT', {local: '%N%n%O%n%A%n%C %Z'}], 35 | ['BY', {local: '%O%n%N%n%A%n%Z, %C%n%S'}], 36 | ['CA', {local: '%N%n%O%n%A%n%C %S %Z'}], 37 | ['CC', {local: '%O%n%N%n%A%n%C %S %Z'}], 38 | ['CH', {local: '%O%n%N%n%A%nCH-%Z %C'}], 39 | ['CI', {local: '%N%n%O%n%X %A %C %X'}], 40 | ['CL', {local: '%N%n%O%n%A%n%Z %C%n%S'}], 41 | ['CN', {local: '%Z%n%S%C%D%n%A%n%O%n%N', latin: '%N%n%O%n%A%n%D%n%C%n%S, %Z'}], 42 | ['CO', {local: '%N%n%O%n%A%n%D%n%C, %S, %Z'}], 43 | ['CR', {local: '%N%n%O%n%A%n%S, %C%n%Z'}], 44 | ['CU', {local: '%N%n%O%n%A%n%C %S%n%Z'}], 45 | ['CV', {local: '%N%n%O%n%A%n%Z %C%n%S'}], 46 | ['CX', {local: '%O%n%N%n%A%n%C %S %Z'}], 47 | ['CY', {local: '%N%n%O%n%A%n%Z %C'}], 48 | ['CZ', {local: '%N%n%O%n%A%n%Z %C'}], 49 | ['DE', {local: '%N%n%O%n%A%n%Z %C'}], 50 | ['DK', {local: '%N%n%O%n%A%n%Z %C'}], 51 | ['DO', {local: '%N%n%O%n%A%n%Z %C'}], 52 | ['DZ', {local: '%N%n%O%n%A%n%Z %C'}], 53 | ['EC', {local: '%N%n%O%n%A%n%Z%n%C'}], 54 | ['EE', {local: '%N%n%O%n%A%n%Z %C %S'}], 55 | ['EG', {local: '%N%n%O%n%A%n%C%n%S%n%Z', latin: '%N%n%O%n%A%n%C%n%S%n%Z'}], 56 | ['EH', {local: '%N%n%O%n%A%n%Z %C'}], 57 | ['ES', {local: '%N%n%O%n%A%n%Z %C %S'}], 58 | ['ET', {local: '%N%n%O%n%A%n%Z %C'}], 59 | ['FI', {local: '%O%n%N%n%A%nFI-%Z %C'}], 60 | ['FK', {local: '%N%n%O%n%A%n%C%n%Z'}], 61 | ['FM', {local: '%N%n%O%n%A%n%C %S %Z'}], 62 | ['FO', {local: '%N%n%O%n%A%nFO%Z %C'}], 63 | ['FR', {local: '%O%n%N%n%A%n%Z %C'}], 64 | ['GB', {local: '%N%n%O%n%A%n%C%n%Z'}], 65 | ['GE', {local: '%N%n%O%n%A%n%Z %C'}], 66 | ['GF', {local: '%O%n%N%n%A%n%Z %C %X'}], 67 | ['GG', {local: '%N%n%O%n%A%n%C%nGUERNSEY%n%Z'}], 68 | ['GI', {local: '%N%n%O%n%A%nGIBRALTAR%n%Z'}], 69 | ['GL', {local: '%N%n%O%n%A%n%Z %C'}], 70 | ['GN', {local: '%N%n%O%n%Z %A %C'}], 71 | ['GP', {local: '%O%n%N%n%A%n%Z %C %X'}], 72 | ['GR', {local: '%N%n%O%n%A%n%Z %C'}], 73 | ['GS', {local: '%N%n%O%n%A%n%n%C%n%Z'}], 74 | ['GT', {local: '%N%n%O%n%A%n%Z- %C'}], 75 | ['GU', {local: '%N%n%O%n%A%n%C %Z'}], 76 | ['GW', {local: '%N%n%O%n%A%n%Z %C'}], 77 | ['HK', {local: '%S%n%C%n%A%n%O%n%N', latin: '%N%n%O%n%A%n%C%n%S'}], 78 | ['HM', {local: '%O%n%N%n%A%n%C %S %Z'}], 79 | ['HN', {local: '%N%n%O%n%A%n%C, %S%n%Z'}], 80 | ['HR', {local: '%N%n%O%n%A%nHR-%Z %C'}], 81 | ['HT', {local: '%N%n%O%n%A%nHT%Z %C'}], 82 | ['HU', {local: '%N%n%O%n%C%n%A%n%Z'}], 83 | ['ID', {local: '%N%n%O%n%A%n%C%n%S %Z'}], 84 | ['IE', {local: '%N%n%O%n%A%n%D%n%C%n%S%n%Z'}], 85 | ['IL', {local: '%N%n%O%n%A%n%C %Z'}], 86 | ['IM', {local: '%N%n%O%n%A%n%C%n%Z'}], 87 | ['IN', {local: '%N%n%O%n%A%n%C %Z%n%S'}], 88 | ['IO', {local: '%N%n%O%n%A%n%C%n%Z'}], 89 | ['IQ', {local: '%O%n%N%n%A%n%C, %S%n%Z'}], 90 | ['IR', {local: '%O%n%N%n%S%n%C, %D%n%A%n%Z'}], 91 | ['IS', {local: '%N%n%O%n%A%n%Z %C'}], 92 | ['IT', {local: '%N%n%O%n%A%n%Z %C %S'}], 93 | ['JE', {local: '%N%n%O%n%A%n%C%nJERSEY%n%Z'}], 94 | ['JM', {local: '%N%n%O%n%A%n%C%n%S %X'}], 95 | ['JO', {local: '%N%n%O%n%A%n%C %Z'}], 96 | ['JP', {local: '〒%Z%n%S%n%A%n%O%n%N', latin: '%N%n%O%n%A, %S%n%Z'}], 97 | ['KE', {local: '%N%n%O%n%A%n%C%n%Z'}], 98 | ['KG', {local: '%N%n%O%n%A%n%Z %C'}], 99 | ['KH', {local: '%N%n%O%n%A%n%C %Z'}], 100 | ['KI', {local: '%N%n%O%n%A%n%S%n%C'}], 101 | ['KN', {local: '%N%n%O%n%A%n%C, %S'}], 102 | ['KP', {local: '%Z%n%S%n%C%n%A%n%O%n%N', latin: '%N%n%O%n%A%n%C%n%S, %Z'}], 103 | ['KR', {local: '%S %C%D%n%A%n%O%n%N%n%Z', latin: '%N%n%O%n%A%n%D%n%C%n%S%n%Z'}], 104 | ['KW', {local: '%N%n%O%n%A%n%Z %C'}], 105 | ['KY', {local: '%N%n%O%n%A%n%S %Z'}], 106 | ['KZ', {local: '%Z%n%S%n%C%n%A%n%O%n%N'}], 107 | ['LA', {local: '%N%n%O%n%A%n%Z %C'}], 108 | ['LB', {local: '%N%n%O%n%A%n%C %Z'}], 109 | ['LI', {local: '%O%n%N%n%A%nFL-%Z %C'}], 110 | ['LK', {local: '%N%n%O%n%A%n%C%n%Z'}], 111 | ['LR', {local: '%N%n%O%n%A%n%Z %C'}], 112 | ['LS', {local: '%N%n%O%n%A%n%C %Z'}], 113 | ['LT', {local: '%O%n%N%n%A%nLT-%Z %C %S'}], 114 | ['LU', {local: '%O%n%N%n%A%nL-%Z %C'}], 115 | ['LV', {local: '%N%n%O%n%A%n%S%n%C, %Z'}], 116 | ['MA', {local: '%N%n%O%n%A%n%Z %C'}], 117 | ['MC', {local: '%N%n%O%n%A%nMC-%Z %C %X'}], 118 | ['MD', {local: '%N%n%O%n%A%nMD-%Z %C'}], 119 | ['ME', {local: '%N%n%O%n%A%n%Z %C'}], 120 | ['MF', {local: '%O%n%N%n%A%n%Z %C %X'}], 121 | ['MG', {local: '%N%n%O%n%A%n%Z %C'}], 122 | ['MH', {local: '%N%n%O%n%A%n%C %S %Z'}], 123 | ['MK', {local: '%N%n%O%n%A%n%Z %C'}], 124 | ['MM', {local: '%N%n%O%n%A%n%C, %Z'}], 125 | ['MN', {local: '%N%n%O%n%A%n%C%n%S %Z'}], 126 | ['MO', {local: '%A%n%O%n%N', latin: '%N%n%O%n%A'}], 127 | ['MP', {local: '%N%n%O%n%A%n%C %S %Z'}], 128 | ['MQ', {local: '%O%n%N%n%A%n%Z %C %X'}], 129 | ['MT', {local: '%N%n%O%n%A%n%C %Z'}], 130 | ['MU', {local: '%N%n%O%n%A%n%Z%n%C'}], 131 | ['MV', {local: '%N%n%O%n%A%n%C %Z'}], 132 | ['MW', {local: '%N%n%O%n%A%n%C %X'}], 133 | ['MX', {local: '%N%n%O%n%A%n%D%n%Z %C, %S'}], 134 | ['MY', {local: '%N%n%O%n%A%n%D%n%Z %C%n%S'}], 135 | ['MZ', {local: '%N%n%O%n%A%n%Z %C%S'}], 136 | ['NA', {local: '%N%n%O%n%A%n%C%n%Z'}], 137 | ['NC', {local: '%O%n%N%n%A%n%Z %C %X'}], 138 | ['NE', {local: '%N%n%O%n%A%n%Z %C'}], 139 | ['NF', {local: '%O%n%N%n%A%n%C %S %Z'}], 140 | ['NG', {local: '%N%n%O%n%A%n%D%n%C %Z%n%S'}], 141 | ['NI', {local: '%N%n%O%n%A%n%Z%n%C, %S'}], 142 | ['NL', {local: '%O%n%N%n%A%n%Z %C'}], 143 | ['NO', {local: '%N%n%O%n%A%n%Z %C'}], 144 | ['NP', {local: '%N%n%O%n%A%n%C %Z'}], 145 | ['NR', {local: '%N%n%O%n%A%n%S'}], 146 | ['NZ', {local: '%N%n%O%n%A%n%D%n%C %Z'}], 147 | ['OM', {local: '%N%n%O%n%A%n%Z%n%C'}], 148 | ['PA', {local: '%N%n%O%n%A%n%C%n%S'}], 149 | ['PE', {local: '%N%n%O%n%A%n%C %Z%n%S'}], 150 | ['PF', {local: '%N%n%O%n%A%n%Z %C %S'}], 151 | ['PG', {local: '%N%n%O%n%A%n%C %Z %S'}], 152 | ['PH', {local: '%N%n%O%n%A%n%D, %C%n%Z %S'}], 153 | ['PK', {local: '%N%n%O%n%A%n%D%n%C-%Z'}], 154 | ['PL', {local: '%N%n%O%n%A%n%Z %C'}], 155 | ['PM', {local: '%O%n%N%n%A%n%Z %C %X'}], 156 | ['PN', {local: '%N%n%O%n%A%n%C%n%Z'}], 157 | ['PR', {local: '%N%n%O%n%A%n%C PR %Z'}], 158 | ['PT', {local: '%N%n%O%n%A%n%Z %C'}], 159 | ['PW', {local: '%N%n%O%n%A%n%C %S %Z'}], 160 | ['PY', {local: '%N%n%O%n%A%n%Z %C'}], 161 | ['RE', {local: '%O%n%N%n%A%n%Z %C %X'}], 162 | ['RO', {local: '%N%n%O%n%A%n%Z %S %C'}], 163 | ['RS', {local: '%N%n%O%n%A%n%Z %C'}], 164 | ['RU', {local: '%N%n%O%n%A%n%C%n%S%n%Z', latin: '%N%n%O%n%A%n%C%n%S%n%Z'}], 165 | ['SA', {local: '%N%n%O%n%A%n%C %Z'}], 166 | ['SC', {local: '%N%n%O%n%A%n%C%n%S'}], 167 | ['SD', {local: '%N%n%O%n%A%n%C%n%Z'}], 168 | ['SE', {local: '%O%n%N%n%A%nSE-%Z %C'}], 169 | ['SG', {local: '%N%n%O%n%A%nSINGAPORE %Z'}], 170 | ['SH', {local: '%N%n%O%n%A%n%C%n%Z'}], 171 | ['SI', {local: '%N%n%O%n%A%nSI-%Z %C'}], 172 | ['SJ', {local: '%N%n%O%n%A%n%Z %C'}], 173 | ['SK', {local: '%N%n%O%n%A%n%Z %C'}], 174 | ['SM', {local: '%N%n%O%n%A%n%Z %C'}], 175 | ['SN', {local: '%N%n%O%n%A%n%Z %C'}], 176 | ['SO', {local: '%N%n%O%n%A%n%C, %S %Z'}], 177 | ['SR', {local: '%N%n%O%n%A%n%C%n%S'}], 178 | ['SV', {local: '%N%n%O%n%A%n%Z-%C%n%S'}], 179 | ['SZ', {local: '%N%n%O%n%A%n%C%n%Z'}], 180 | ['TA', {local: '%N%n%O%n%A%n%C%n%Z'}], 181 | ['TC', {local: '%N%n%O%n%A%n%C%n%Z'}], 182 | ['TH', {local: '%N%n%O%n%A%n%D %C%n%S %Z', latin: '%N%n%O%n%A%n%D, %C%n%S %Z'}], 183 | ['TJ', {local: '%N%n%O%n%A%n%Z %C'}], 184 | ['TM', {local: '%N%n%O%n%A%n%Z %C'}], 185 | ['TN', {local: '%N%n%O%n%A%n%Z %C'}], 186 | ['TR', {local: '%N%n%O%n%A%n%Z %C/%S'}], 187 | ['TV', {local: '%N%n%O%n%A%n%C%n%S'}], 188 | ['TW', {local: '%Z%n%S%C%n%A%n%O%n%N', latin: '%N%n%O%n%A%n%C, %S %Z'}], 189 | ['TZ', {local: '%N%n%O%n%A%n%Z %C'}], 190 | ['UA', {local: '%N%n%O%n%A%n%C%n%S%n%Z', latin: '%N%n%O%n%A%n%C%n%S%n%Z'}], 191 | ['UM', {local: '%N%n%O%n%A%n%C %S %Z'}], 192 | ['US', {local: '%N%n%O%n%A%n%C, %S %Z'}], 193 | ['UY', {local: '%N%n%O%n%A%n%Z %C %S'}], 194 | ['UZ', {local: '%N%n%O%n%A%n%Z %C%n%S'}], 195 | ['VA', {local: '%N%n%O%n%A%n%Z %C'}], 196 | ['VC', {local: '%N%n%O%n%A%n%C %Z'}], 197 | ['VE', {local: '%N%n%O%n%A%n%C %Z, %S'}], 198 | ['VG', {local: '%N%n%O%n%A%n%C%n%Z'}], 199 | ['VI', {local: '%N%n%O%n%A%n%C %S %Z'}], 200 | ['VN', {local: '%N%n%O%n%A%n%C%n%S %Z', latin: '%N%n%O%n%A%n%C%n%S %Z'}], 201 | ['WF', {local: '%O%n%N%n%A%n%Z %C %X'}], 202 | ['XK', {local: '%N%n%O%n%A%n%Z %C'}], 203 | ['YT', {local: '%O%n%N%n%A%n%Z %C %X'}], 204 | ['ZA', {local: '%N%n%O%n%A%n%D%n%C%n%Z'}], 205 | ['ZM', {local: '%N%n%O%n%A%n%Z %C'}], 206 | ]); 207 | 208 | export const defaultAddressFormat = '%N%n%O%n%A%n%C'; 209 | 210 | export default addressFormats; 211 | --------------------------------------------------------------------------------