├── .editorconfig ├── .eslintignore ├── .eslintrc.cjs ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── pull_request_template.md └── workflows │ ├── e2e-tests.yml │ └── unit-tests.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .npmrc ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── .vscode ├── extensions.json └── settings.json ├── CONTRIBUTING.md ├── README.md ├── commitlint.config.cjs ├── eslint.config.js ├── messages ├── en.json └── sv.json ├── package.json ├── playwright.config.ts ├── pnpm-lock.yaml ├── project.inlang ├── .gitignore ├── project_id └── settings.json ├── src ├── app.d.ts ├── app.html ├── hooks.server.ts ├── hooks.ts ├── lib │ ├── components │ │ ├── pages │ │ │ ├── Collator.svelte │ │ │ ├── DateTimeFormat.svelte │ │ │ ├── DisplayNames.svelte │ │ │ ├── DurationFormat.svelte │ │ │ ├── ListFormat.svelte │ │ │ ├── Locale.svelte │ │ │ ├── NumberFormat.Currency.svelte │ │ │ ├── NumberFormat.Unit.svelte │ │ │ ├── NumberFormat.svelte │ │ │ ├── PageLayout.svelte │ │ │ ├── Playground │ │ │ │ ├── Playground.svelte │ │ │ │ ├── PlaygroundInput.svelte │ │ │ │ ├── PlaygroundOptions.svelte │ │ │ │ └── PlaygroundSecondaryFormatters.svelte │ │ │ ├── PluralRules.svelte │ │ │ ├── RelativeTimeFormat.svelte │ │ │ └── Segmenter.svelte │ │ └── ui │ │ │ ├── BrowserSupport │ │ │ ├── BrowserSupport.svelte │ │ │ ├── BrowserSupportGrid.svelte │ │ │ ├── SpecificationUrls.svelte │ │ │ └── SupportLabel.svelte │ │ │ ├── Button.svelte │ │ │ ├── Card.svelte │ │ │ ├── Checkbox.svelte │ │ │ ├── Chip.svelte │ │ │ ├── CodeBlock.svelte │ │ │ ├── ComboBox.svelte │ │ │ ├── DateTime.svelte │ │ │ ├── Dialog.svelte │ │ │ ├── DisplayNamesHighlight.svelte │ │ │ ├── Fieldset.svelte │ │ │ ├── Grid.svelte │ │ │ ├── Header.svelte │ │ │ ├── Highlight.svelte │ │ │ ├── Highlight │ │ │ ├── HighlightLocale.svelte │ │ │ └── Token.svelte │ │ │ ├── HighlightValue.svelte │ │ │ ├── Input.svelte │ │ │ ├── LocalePicker.svelte │ │ │ ├── MDNLink.svelte │ │ │ ├── Main.svelte │ │ │ ├── Navigation.svelte │ │ │ ├── OptionCard.svelte │ │ │ ├── OptionHeader.svelte │ │ │ ├── OptionSection.svelte │ │ │ ├── ProgressBar.svelte │ │ │ ├── Radio.svelte │ │ │ ├── Select.svelte │ │ │ ├── SettingsDialog.svelte │ │ │ ├── Skeleton.svelte │ │ │ ├── SkipLink.svelte │ │ │ ├── Slider.svelte │ │ │ ├── Spacing.svelte │ │ │ ├── SrOnly.svelte │ │ │ ├── details │ │ │ └── Details.svelte │ │ │ └── icons │ │ │ ├── Add.svelte │ │ │ ├── AndroidWebView.svelte │ │ │ ├── Browser.svelte │ │ │ ├── BrowserType.svelte │ │ │ ├── Check.svelte │ │ │ ├── ChevronUp.svelte │ │ │ ├── CopyToClipboard.svelte │ │ │ ├── Deno.svelte │ │ │ ├── Desktop.svelte │ │ │ ├── Edge.svelte │ │ │ ├── Edit.svelte │ │ │ ├── Firefox.svelte │ │ │ ├── Icon.svelte │ │ │ ├── Mobile.svelte │ │ │ ├── Moon.svelte │ │ │ ├── Node.svelte │ │ │ ├── OpenInNewTab.svelte │ │ │ ├── Opera.svelte │ │ │ ├── Safari.svelte │ │ │ ├── Samsung.svelte │ │ │ ├── Server.svelte │ │ │ ├── Settings.svelte │ │ │ ├── Sun.svelte │ │ │ ├── Times.svelte │ │ │ └── chrome.svelte │ ├── constants.ts │ ├── format-methods.ts │ ├── format-options │ │ ├── collator.options.ts │ │ ├── common.options.ts │ │ ├── datetime-format.options.ts │ │ ├── display-names.options.ts │ │ ├── duration-format.options.ts │ │ ├── index.ts │ │ ├── list-format.options.ts │ │ ├── number-format.options.ts │ │ ├── plural-rules.options.ts │ │ ├── relative-time-format.options.ts │ │ ├── segmenter.options.ts │ │ └── types.ts │ ├── live-announcer │ │ ├── constants.ts │ │ ├── live-announcer-region.svelte │ │ ├── live-announcer.svelte │ │ ├── types.ts │ │ └── util.ts │ ├── locale-data │ │ ├── calendars.ts │ │ ├── currencies.ts │ │ ├── locales.ts │ │ └── units.ts │ ├── playground │ │ ├── format.utils.test.ts │ │ ├── format.utils.ts │ │ ├── playground.schema.ts │ │ ├── schemas │ │ │ ├── collator.schema.ts │ │ │ ├── dateTimeFormat.schema.ts │ │ │ ├── displayNames.schema.ts │ │ │ ├── durationFormat.schema.ts │ │ │ ├── index.ts │ │ │ ├── listFormat.schema.ts │ │ │ ├── numberFormat.schema.ts │ │ │ ├── pluralRules.schema.ts │ │ │ ├── relativeTimeFormat.schema.ts │ │ │ └── segmenter.schema.ts │ │ ├── url.utils.ts │ │ ├── validate.test.ts │ │ └── validate.ts │ ├── routes.ts │ ├── store │ │ ├── locales.ts │ │ └── settings.ts │ ├── types │ │ ├── BrowserSupport.types.ts │ │ ├── OptionValues.types.ts │ │ └── common.ts │ └── utils │ │ ├── analytics.ts │ │ ├── browser-support.ts │ │ ├── copy-to-clipboard.ts │ │ ├── directives.ts │ │ ├── dom-utils.ts │ │ ├── factory.ts │ │ ├── format-utils.test.ts │ │ ├── format-utils.ts │ │ ├── get-locale.ts │ │ ├── load-json.ts │ │ └── write-compat-data.ts ├── routes │ ├── +layout.svelte │ ├── +page.svelte │ ├── Collator │ │ └── +page.svelte │ ├── DateTimeFormat │ │ └── +page.svelte │ ├── DisplayNames │ │ └── +page.svelte │ ├── DurationFormat │ │ └── +page.svelte │ ├── ListFormat │ │ └── +page.svelte │ ├── Locale │ │ └── +page.svelte │ ├── NumberFormat │ │ ├── +page.svelte │ │ ├── Currency │ │ │ └── +page.svelte │ │ └── Unit │ │ │ └── +page.svelte │ ├── Playground │ │ └── +page.svelte │ ├── PluralRules │ │ └── +page.svelte │ ├── RelativeTimeFormat │ │ └── +page.svelte │ ├── Segmenter │ │ └── +page.svelte │ └── routing-links.svelte └── update-compat-data.ts ├── static ├── Collator-compat-data.json ├── DateTimeFormat-compat-data.json ├── DisplayNames-compat-data.json ├── DurationFormat-compat-data.json ├── ListFormat-compat-data.json ├── Locale-compat-data.json ├── NumberFormat-compat-data.json ├── NumberFormat │ └── NumberFormat-compat-data.json ├── Playground-compat-data.json ├── PluralRules-compat-data.json ├── RelativeTimeFormat-compat-data.json ├── Segmenter-compat-data.json ├── favicon.svg ├── icons │ ├── chrome_partial_support.svg │ ├── chrome_supported.svg │ ├── chrome_unsupported.svg │ ├── deno_partial_support.svg │ ├── deno_supported.svg │ ├── deno_unsupported.svg │ ├── desktop.svg │ ├── edge_partial_support.svg │ ├── edge_supported.svg │ ├── edge_unsupported.svg │ ├── experimental.svg │ ├── firefox_partial_support.svg │ ├── firefox_supported.svg │ ├── firefox_unsupported.svg │ ├── mobile.svg │ ├── nodejs_partial_support.svg │ ├── nodejs_supported.svg │ ├── nodejs_unsupported.svg │ ├── opera_partial_support.svg │ ├── opera_supported.svg │ ├── opera_unsupported.svg │ ├── safari_partial_support.svg │ ├── safari_supported.svg │ ├── safari_unsupported.svg │ ├── samsunginternet_partial_support.svg │ ├── samsunginternet_supported.svg │ ├── samsunginternet_unsupported.svg │ ├── server.svg │ ├── webview_partial_support.svg │ ├── webview_supported.svg │ └── webview_unsupported.svg └── social.png ├── svelte.config.js ├── tests ├── constants.ts ├── methods.spec.ts ├── pages │ ├── BasePage.ts │ ├── IntlPage.ts │ ├── PlaygroundPage.ts │ └── SettingsPage.ts ├── playground.spec.ts ├── settings.spec.ts └── test.ts ├── tsconfig.json └── vite.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | indent_style = tab 8 | indent_size = 2 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | 10 | # Ignore files for PNPM, NPM and YARN 11 | pnpm-lock.yaml 12 | package-lock.json 13 | yarn.lock 14 | .cjs -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import("eslint").Linter.Config} */ 2 | module.exports = { 3 | root: true, 4 | extends: [ 5 | "eslint:recommended", 6 | "plugin:@typescript-eslint/recommended", 7 | "plugin:svelte/recommended", 8 | "plugin:svelte/prettier", 9 | "prettier" 10 | ], 11 | parser: "@typescript-eslint/parser", 12 | plugins: ["@typescript-eslint"], 13 | parserOptions: { 14 | sourceType: "module", 15 | ecmaVersion: 2020, 16 | extraFileExtensions: [".svelte"] 17 | }, 18 | env: { 19 | browser: true, 20 | es2017: true, 21 | node: true 22 | }, 23 | rules: { 24 | "@typescript-eslint/no-unused-vars": ["error", { varsIgnorePattern: "Slots" }] 25 | }, 26 | overrides: [ 27 | { 28 | files: ["*.svelte"], 29 | parser: "svelte-eslint-parser", 30 | parserOptions: { 31 | parser: "@typescript-eslint/parser" 32 | } 33 | } 34 | ] 35 | }; 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | 28 | - OS: [e.g. iOS] 29 | - Browser [e.g. chrome, safari] 30 | - Version [e.g. 22] 31 | 32 | **Smartphone (please complete the following information):** 33 | 34 | - Device: [e.g. iPhone6] 35 | - OS: [e.g. iOS8.1] 36 | - Browser [e.g. stock browser, safari] 37 | - Version [e.g. 22] 38 | 39 | **Additional context** 40 | Add any other context about the problem here. 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## What kind of change does this PR introduce? 2 | 3 | ## What is the current behavior? 4 | > You can also link to an open issue here 5 | 6 | ## What is the new behavior? 7 | 8 | ## Other information 9 | -------------------------------------------------------------------------------- /.github/workflows/e2e-tests.yml: -------------------------------------------------------------------------------- 1 | name: e2e tests 2 | on: 3 | pull_request: 4 | types: [opened, edited, reopened, synchronize] 5 | branches: 6 | - main 7 | paths-ignore: 8 | - '**.md' 9 | - '.vscode/**' 10 | jobs: 11 | test: 12 | timeout-minutes: 60 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - uses: pnpm/action-setup@v4 17 | with: 18 | version: 9 19 | - name: Use Node.js 20 20 | uses: actions/setup-node@v2 21 | with: 22 | node-version: 20 23 | cache: 'pnpm' 24 | - name: Install dependencies 25 | run: pnpm install 26 | - name: Install Playwright 27 | run: npx playwright install --with-deps 28 | - name: Run Playwright tests 29 | run: npx playwright test 30 | - uses: actions/upload-artifact@v4 31 | if: ${{ !cancelled() }} 32 | with: 33 | name: playwright-report 34 | path: playwright-report/ 35 | retention-days: 30 36 | -------------------------------------------------------------------------------- /.github/workflows/unit-tests.yml: -------------------------------------------------------------------------------- 1 | name: unit tests 2 | on: 3 | pull_request: 4 | types: [opened, edited, reopened, synchronize] 5 | branches: 6 | - main 7 | paths-ignore: 8 | - '**.md' 9 | - '.vscode/**' 10 | jobs: 11 | test: 12 | timeout-minutes: 60 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - uses: pnpm/action-setup@v4 17 | with: 18 | version: 9 19 | - name: Use Node.js 20 20 | uses: actions/setup-node@v2 21 | with: 22 | node-version: 20 23 | cache: 'pnpm' 24 | - name: Install dependencies 25 | run: pnpm install 26 | - name: Run tests 27 | run: pnpm test 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | 10 | .netlify 11 | .vercel 12 | test-results 13 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx --no -- commitlint --edit "" 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | auto-install-peers=true 3 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 20.11.0 -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | 10 | # Ignore files for PNPM, NPM and YARN 11 | pnpm-lock.yaml 12 | package-lock.json 13 | yarn.lock 14 | format.utils.ts 15 | static/*.json 16 | /.vercel 17 | /.github 18 | 19 | CodeBlock.svelte 20 | HighlightValue.svelte 21 | Highlight.svelte 22 | Token.svelte 23 | .cjs 24 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": false, 4 | "tabWidth": 2, 5 | "trailingComma": "none", 6 | "printWidth": 100, 7 | "plugins": ["prettier-plugin-svelte"], 8 | "overrides": [ 9 | { 10 | "files": "*.svelte", 11 | "options": { 12 | "parser": "svelte" 13 | } 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["inlang.vs-code-extension"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "[svelte]": { 4 | "editor.defaultFormatter": "svelte.svelte-vscode" 5 | }, 6 | "editor.codeActionsOnSave": { 7 | "source.fixAll": "always" 8 | }, 9 | "javascript.preferences.importModuleSpecifier": "non-relative", 10 | "workbench.editor.customLabels.patterns": { 11 | "**/+*": "${dirname}/${filename}.${extname}" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Intl Explorer 2 | 3 | A big welcome and thank you for considering contributing to _Intl Explorer_! 4 | 5 | Reading and following these guidelines will help us make the contribution process easy and effective for everyone involved. It also communicates that you agree to respect the time of the developers managing and developing these open source projects. In return, we will reciprocate that respect by addressing your issue, assessing changes, and helping you finalize your pull requests. 6 | 7 | ## Quicklinks 8 | 9 | - [Getting Started](#getting-started) 10 | - [Issues](#issues) 11 | - [Pull Requests](#pull-requests) 12 | - [Adding Translations](#adding-translations) 13 | - [License](#license) 14 | 15 | ## Getting Started 16 | 17 | Contributions are made to this repo via Issues and Pull Requests (PRs). If you are unsure about anything when contributing do not hesitate to open an Issue and ask what your next step should be. 18 | 19 | ### Issues 20 | 21 | Issues should be used to report problems with the website, request a new feature, or to discuss potential changes before a PR is created. 22 | 23 | If you find an Issue that addresses the problem you're having, please add your own reproduction information to the existing issue rather than creating a new one. Adding a [reaction](https://github.blog/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/) can also help be indicating to our maintainers that a particular problem is affecting more than just the reporter. 24 | 25 | ### Pull Requests 26 | 27 | PRs to the repository are always welcome and can be a quick way to get your fix or improvement slated for the next release. In general, PRs should: 28 | 29 | - Only fix/add the functionality in question. 30 | - Add tests for fixed or changed functionality (if a test suite already exists). 31 | 32 | In general, we follow the ["fork-and-pull" Git workflow](https://github.com/susam/gitpr) 33 | 34 | 1. Fork the repository to your own Github account 35 | 2. Clone the project to your machine 36 | 3. Create a branch locally with a succinct but descriptive name 37 | 4. Be sure to lint and format before commiting (`pnpm format`, `pnpm lint`) and fix any issues that the linter finds. 38 | 5. Commit changes to the branch. We use [commitlint](https://github.com/conventional-changelog/commitlint) to check if your commit message follows [conventional commits format](https://www.conventionalcommits.org/en/v1.0.0/) 39 | 6. Push changes to your fork 40 | 7. Open a PR in our repository and follow the PR template so that we can efficiently review the changes. 41 | 42 | ## Adding Translations 43 | 44 | 1. Fork and clone the repository to your local machine 45 | 2. Create a new branch for your translation with the name `feat/translations/` (e.g. `feat/translations/es`) 46 | 3. Add your language code to the property `languageTags` to the file [./project.inlang/settings.json](./project.inlang/settings.json) 47 | 4. Add a file with the name of your language code in the folder [./messages](./messages) (e.g. `es.json`) 48 | 5. Copy contents of the file [./messages/en.json](./messages/en.json) to your new file and start translating the strings. 49 | 6. Run project locally and check if your translations are working as expected. 50 | 51 | - You can change language under "Settings" in the top right corner of the page. 52 | 53 | 7. Commit your changes and push to your fork and open a PR to this repository. 54 | 55 | ## License 56 | 57 | By contributing, you agree that your contributions will be licensed under its MIT License. 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![social](https://github.com/jesperorb/intl-explorer/assets/21122051/4e370c44-803f-491c-b176-0a952b56b0ee) 2 | 3 | # Intl Explorer 4 | 5 | Intl Explorer is an interactive tool for experimenting and trying out the [ECMAScript Internationalization API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl). The API is available under the namespace Intl in JavaScript and provides language sensitive string comparison, number formatting, and date and time formatting. 6 | 7 | ## Live site 8 | 9 | **[https://intl-explorer.com/](https://intl-explorer.com/)** 10 | 11 | ## Running 12 | 13 | > This repo uses [pnpm](https://pnpm.io/) 14 | 15 | 1. Install dependencies: `pnpm install`. 16 | 2. Run devserver: `pnpm dev`. 17 | 3. Visit: [http://127.0.0.1:5173/](http://127.0.0.1:5173/). 18 | 19 | ## Building 20 | 21 | 1. Run command: `pnpm build`. 22 | 23 | ## Contributing 24 | 25 | See the [contributing guide](CONTRIBUTING.md) for detailed instructions on how to get started with our project. 26 | -------------------------------------------------------------------------------- /commitlint.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["@commitlint/config-conventional"] 3 | }; 4 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import prettier from "eslint-config-prettier"; 2 | import js from "@eslint/js"; 3 | import { includeIgnoreFile } from "@eslint/compat"; 4 | import svelte from "eslint-plugin-svelte"; 5 | import globals from "globals"; 6 | import { fileURLToPath } from "node:url"; 7 | import ts from "typescript-eslint"; 8 | import svelteConfig from "./svelte.config.js"; 9 | 10 | const gitignorePath = fileURLToPath(new URL("./.gitignore", import.meta.url)); 11 | 12 | export default ts.config( 13 | includeIgnoreFile(gitignorePath), 14 | js.configs.recommended, 15 | ...ts.configs.recommended, 16 | ...svelte.configs.recommended, 17 | prettier, 18 | ...svelte.configs.prettier, 19 | { 20 | languageOptions: { 21 | globals: { ...globals.browser, ...globals.node } 22 | }, 23 | rules: { "no-undef": "off" } 24 | }, 25 | { 26 | files: ["**/*.svelte", "**/*.svelte.ts", "**/*.svelte.js"], 27 | ignores: ["eslint.config.js", "svelte.config.js"], 28 | languageOptions: { 29 | parserOptions: { 30 | projectService: true, 31 | extraFileExtensions: [".svelte"], 32 | parser: ts.parser, 33 | svelteConfig 34 | } 35 | } 36 | } 37 | ); 38 | -------------------------------------------------------------------------------- /messages/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://inlang.com/schema/inlang-message-format", 3 | "about": "About", 4 | "accentColor": "Accent color", 5 | "accentColorHint": "Change the color of the accent (default: purple)", 6 | "alternativeUseHeading": "Alternativ use", 7 | "alternativeUseIngress": "{method} can also be used from", 8 | "amount": "Amount", 9 | "and": "and", 10 | "announceOutputToScreenreader": "Announce output to screen reader", 11 | "announceOutputToScreenreaderHint": "Disable to stop screen reader from announcing playground output on input change", 12 | "availableInBrowser": "Available in {browserName} from version {versionAdded}", 13 | "blurbPart1": "is a tool for experimenting and trying out the", 14 | "blurbPart2": "The API is available under the namespace", 15 | "blurbPart3": "in JavaScript and provides language sensitive datetime formatting, number formatting, string comparison and more.", 16 | "blurbPart4": "To get started either visit", 17 | "blurbPart5": "or choose formatter you are interested in from the navigation menu.", 18 | "blurbPart6": "The source code for this tool can be found at", 19 | "blurbPart7": "The creator of this tool is", 20 | "blurbWelcome": "Welcome to Intl Explorer!", 21 | "browserDetails": "Browser details", 22 | "browserSupport": "Browser support", 23 | "close": "Close", 24 | "code": "Code", 25 | "codeTheme": "Code Theme", 26 | "codeThemeHint": "Theme for code blocks: OceanicNext/Github", 27 | "confirmLanguage": "Confirm language", 28 | "copyCode": "Copy Code", 29 | "copyCodeAriaLabel": "Copy code for options: {code}", 30 | "copyCodeDone": "Code copied to clipboard", 31 | "copySchemaUrl": "Copy Schema URL", 32 | "copySchemaUrlDone": "Schema URL copied to clipboard", 33 | "currency": "Currency", 34 | "dark": "Dark", 35 | "date": "Date", 36 | "equals": "equals", 37 | "fullSupport": "Full", 38 | "input": "Input", 39 | "language": "Language", 40 | "languageHint": "Language for page. Press Confirm to reload page with selected language", 41 | "light": "Light", 42 | "list": "List", 43 | "locale": "Locale", 44 | "localePlaceHolder": "Select one or multiple locales", 45 | "menu": "Menu", 46 | "meta": "Meta", 47 | "method": "Method", 48 | "no": "No", 49 | "noResult": "No results found", 50 | "noSupport": "No support", 51 | "notAvailableInBrowser": "Not available in {browserName}", 52 | "noValue": "No value", 53 | "options": "Options", 54 | "output": "Output", 55 | "partialSupport": "Partial", 56 | "remove": "Remove {value}", 57 | "resolvedOptions": "Resolved Options", 58 | "secondaryFormatters": "Secondary Formatters", 59 | "seeAlso": "See also", 60 | "selectCurrency": "Select a currency", 61 | "selectLocaleButton": "Select", 62 | "selectUnit": "Select a unit", 63 | "settingsButton": "Settings", 64 | "settingsHeading": "Settings", 65 | "showBrowserSupport": "Show Browser Support", 66 | "showBrowserSupportHint": "Toggle to hide or show Browser Details/Support", 67 | "skipLink": "Skip to content", 68 | "specification": "Specification", 69 | "theme": "Theme", 70 | "themeHint": "Page theme", 71 | "time": "Time", 72 | "unit": "Unit", 73 | "value": "Value" 74 | } 75 | -------------------------------------------------------------------------------- /messages/sv.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://inlang.com/schema/inlang-message-format", 3 | "about": "Om", 4 | "accentColor": "Accentfärg", 5 | "accentColorHint": "Ändra på accentfärgen (standard: lila)", 6 | "alternativeUseHeading": "Alternativ användning", 7 | "alternativeUseIngress": "{method} kan också användas från", 8 | "amount": "Summa", 9 | "and": "och", 10 | "announceOutputToScreenreader": "Annonsera utdata till skärmläsare", 11 | "announceOutputToScreenreaderHint": "Stäng av för att sluta annonsera utdata till skärmläsare vid ändringar i playground", 12 | "availableInBrowser": "Tillgänglig i {browserName} från version {versionAdded}", 13 | "blurbPart1": "är ett verktyg för att experimentera med", 14 | "blurbPart2": "APIet är tillgängligt under namnet", 15 | "blurbPart3": "i JavaScript och ger en språkkänslig datumformattering, nummerformattering, strängjämförelse med mera.", 16 | "blurbPart4": "För att börja antingen besök", 17 | "blurbPart5": "eller välj en formatterare du är intresserad av i navigationsmenyn.", 18 | "blurbPart6": "Källkoden för detta verktyg hittas på", 19 | "blurbPart7": "Skaparen av detta verktyg är", 20 | "blurbWelcome": "Välkommen till Intl Explorer!", 21 | "browserDetails": "Webbläsardetaljer", 22 | "browserSupport": "Webbläsarstöd", 23 | "close": "Stäng", 24 | "code": "Kod", 25 | "codeTheme": "Kodtema", 26 | "codeThemeHint": "Tema för kodblock: OceanicNext/Github", 27 | "confirmLanguage": "Bekräfta språk", 28 | "copyCode": "Kopiera kod", 29 | "copyCodeAriaLabel": "Kopiera kod för alternativ: {code}", 30 | "copyCodeDone": "Kod kopierad till utklipp", 31 | "copySchemaUrl": "Kopiera Schema URL", 32 | "copySchemaUrlDone": "Schema URL kopierad till utklipp", 33 | "currency": "Valuta", 34 | "dark": "Mörkt", 35 | "date": "Datum", 36 | "equals": "är lika med", 37 | "fullSupport": "Fullt", 38 | "input": "Indata", 39 | "language": "Språk", 40 | "languageHint": "Språk för sidan. Bekräfta språk för att ladda om sidan med valda språk.", 41 | "light": "Ljust", 42 | "list": "Lista", 43 | "locale": "Språk", 44 | "localePlaceHolder": "Välj ett eller flera språk", 45 | "menu": "Meny", 46 | "meta": "Meta", 47 | "method": "Metod", 48 | "no": "Nej", 49 | "noSupport": "Inget stöd", 50 | "notAvailableInBrowser": "Inte tillgänglig i {browserName}", 51 | "noValue": "Inget värde", 52 | "options": "Alternativ", 53 | "output": "Utdata", 54 | "partialSupport": "Delvis", 55 | "remove": "Ta bort {value}", 56 | "resolvedOptions": "Alternativ som används", 57 | "secondaryFormatters": "Sekundära formatterare", 58 | "seeAlso": "Se också", 59 | "selectCurrency": "Välj valuta", 60 | "selectLocaleButton": "Välj", 61 | "selectUnit": "Välj enhet", 62 | "settingsButton": "Inställningar", 63 | "settingsHeading": "Inställningar", 64 | "showBrowserSupport": "Visa Webbläsarstöd", 65 | "showBrowserSupportHint": "Visa eller dölj stöd för webbläsare", 66 | "skipLink": "Hoppa till innehåll", 67 | "specification": "Specifikation", 68 | "theme": "Tema", 69 | "themeHint": "Tema för sida", 70 | "time": "Tid", 71 | "unit": "Enhet", 72 | "value": "Värde" 73 | } 74 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "intl-explorer", 3 | "version": "2.0.0", 4 | "license": "MIT", 5 | "author": { 6 | "name": "Jesper Orb" 7 | }, 8 | "private": false, 9 | "type": "module", 10 | "scripts": { 11 | "start": "vite dev", 12 | "dev": "vite dev", 13 | "build": "vite build", 14 | "preview": "vite preview", 15 | "test": "vitest", 16 | "update:compat": "pnpx vite-node ./src/update-compat-data.ts", 17 | "test:e2e": "NODE_NO_WARNINGS=1 playwright test", 18 | "test:gen": "npx playwright codegen http://localhost:5173/", 19 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 20 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 21 | "lint": "prettier --check . && eslint .", 22 | "format": "prettier --write .", 23 | "prepare": "husky" 24 | }, 25 | "devDependencies": { 26 | "@commitlint/cli": "^19.8.0", 27 | "@commitlint/config-conventional": "^19.8.0", 28 | "@eslint/compat": "^1.2.7", 29 | "@eslint/js": "^9.23.0", 30 | "@inlang/paraglide-js": "2.0.8", 31 | "@mdn/browser-compat-data": "6.0.1", 32 | "@melt-ui/pp": "^0.3.2", 33 | "@melt-ui/svelte": "^0.86.6", 34 | "@playwright/test": "^1.51.1", 35 | "@sveltejs/adapter-cloudflare": "^6.0.1", 36 | "@sveltejs/kit": "2.20.2", 37 | "@sveltejs/vite-plugin-svelte": "^5.0.3", 38 | "@types/node": "^22.13.14", 39 | "eslint": "^9.23.0", 40 | "eslint-config-prettier": "^10.1.1", 41 | "eslint-plugin-svelte": "^3.4.0", 42 | "globals": "^16.0.0", 43 | "highlight.js": "^11.11.1", 44 | "husky": "^9.1.7", 45 | "playwright": "^1.51.1", 46 | "playwright-core": "^1.51.1", 47 | "prettier": "^3.5.3", 48 | "prettier-plugin-svelte": "^3.3.3", 49 | "svelte": "^5.25.3", 50 | "svelte-check": "^4.1.5", 51 | "svelte-highlight": "^7.8.2", 52 | "svelte-preprocess": "^6.0.3", 53 | "typescript": "^5.8.2", 54 | "typescript-eslint": "^8.28.0", 55 | "vite": "^6.2.3", 56 | "vitest": "^3.0.9", 57 | "wrangler": "^4.6.0" 58 | }, 59 | "pnpm": { 60 | "onlyBuiltDependencies": [ 61 | "@sveltejs/kit", 62 | "esbuild", 63 | "sharp", 64 | "svelte-preprocess", 65 | "workerd" 66 | ] 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { devices, type PlaywrightTestConfig } from "@playwright/test"; 2 | import { localBaseURL } from "./tests/constants"; 3 | 4 | const config: PlaywrightTestConfig = { 5 | testDir: "tests", 6 | fullyParallel: true, 7 | forbidOnly: Boolean(process.env.CI), 8 | retries: process.env.CI ? 2 : 0, 9 | workers: process.env.CI ? 1 : undefined, 10 | use: { 11 | baseURL: localBaseURL, 12 | headless: Boolean(process.env.CI), 13 | ignoreHTTPSErrors: true, 14 | permissions: ["clipboard-read"], 15 | locale: "en-US", 16 | trace: "on-first-retry" 17 | }, 18 | webServer: { 19 | command: "pnpm dev", 20 | url: localBaseURL, 21 | reuseExistingServer: !Boolean(process.env.CI) 22 | }, 23 | projects: [ 24 | { 25 | name: "Desktop Chrome", 26 | use: { 27 | ...devices["Desktop Chrome"] 28 | } 29 | } 30 | ] 31 | }; 32 | 33 | export default config; 34 | -------------------------------------------------------------------------------- /project.inlang/.gitignore: -------------------------------------------------------------------------------- 1 | cache -------------------------------------------------------------------------------- /project.inlang/project_id: -------------------------------------------------------------------------------- 1 | h7jzwSZRH2JtXvBQ7F -------------------------------------------------------------------------------- /project.inlang/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://inlang.com/schema/project-settings", 3 | "baseLocale": "en", 4 | "locales": ["en", "sv"], 5 | "modules": [ 6 | "https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@4/dist/index.js", 7 | "https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@2/dist/index.js" 8 | ], 9 | "plugin.inlang.messageFormat": { 10 | "pathPattern": "./messages/{locale}.json" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | %sveltekit.head% 10 | 11 | 12 |
%sveltekit.body%
13 | 14 | 15 | -------------------------------------------------------------------------------- /src/hooks.server.ts: -------------------------------------------------------------------------------- 1 | import type { Handle } from "@sveltejs/kit"; 2 | import { paraglideMiddleware } from "$paraglide/server"; 3 | 4 | // creating a handle to use the paraglide middleware 5 | const paraglideHandle: Handle = ({ event, resolve }) => 6 | paraglideMiddleware(event.request, ({ request: localizedRequest, locale }) => { 7 | event.request = localizedRequest; 8 | return resolve(event, { 9 | transformPageChunk: ({ html }) => { 10 | return html.replace("%lang%", locale); 11 | } 12 | }); 13 | }); 14 | 15 | export const handle: Handle = paraglideHandle; 16 | -------------------------------------------------------------------------------- /src/hooks.ts: -------------------------------------------------------------------------------- 1 | import type { Reroute } from "@sveltejs/kit"; 2 | import { deLocalizeUrl } from "$paraglide/runtime"; 3 | 4 | export const reroute: Reroute = (request) => { 5 | return deLocalizeUrl(request.url).pathname; 6 | }; 7 | -------------------------------------------------------------------------------- /src/lib/components/pages/Collator.svelte: -------------------------------------------------------------------------------- 1 | 36 | 37 | 38 | {#snippet input()} 39 | 40 | {/snippet} 41 | {#snippet output()} 42 | 43 | {#each collatorFormatOptionsArray as [option, values], index} 44 | 49 | {#each values as value} 50 | {#if value !== undefined} 51 | 52 | 63 | {/if} 64 | {/each} 65 | 66 | {/each} 67 | 68 | {/snippet} 69 | 70 | -------------------------------------------------------------------------------- /src/lib/components/pages/DurationFormat.svelte: -------------------------------------------------------------------------------- 1 | 68 | 69 | 70 | {#snippet input()} 71 | 72 | {#each Object.keys(duration) as key} 73 | 74 | {/each} 75 | {/snippet} 76 | {#snippet output()} 77 | 78 | {#each durationFormatOptionsArray as [option, values], index} 79 | 84 | {#each values as value} 85 | {#if value !== undefined} 86 | 87 | 92 | {/if} 93 | {/each} 94 | 95 | {/each} 96 | 97 | {/snippet} 98 | 99 | -------------------------------------------------------------------------------- /src/lib/components/pages/Locale.svelte: -------------------------------------------------------------------------------- 1 | 44 | 45 | 46 | {#snippet input()} 47 | 48 | {/snippet} 49 | {#snippet output()} 50 | 51 | {#if intl} 52 | {#each properties as property, index} 53 | 59 | 60 | 61 | 62 | 63 | {/each} 64 | {/if} 65 | 66 | {/snippet} 67 | 68 | -------------------------------------------------------------------------------- /src/lib/components/pages/PageLayout.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 | 20 |

{m.input()}

21 | 22 |
23 | 24 | 25 | {#if showLocalePicker} 26 | 27 | {/if} 28 | {#if input} 29 | {@render input?.()} 30 | {/if} 31 | 32 |
33 | 34 | {#if alternativeUse} 35 | 36 |

{m.alternativeUseHeading()}

37 | 38 | {@render alternativeUse?.()} 39 | 40 | {@render alternativeCode?.()} 41 |
42 | 43 | {/if} 44 |

{m.output()}

45 | 46 |
47 | 48 | {@render output?.()} 49 | -------------------------------------------------------------------------------- /src/lib/components/pages/Playground/PlaygroundInput.svelte: -------------------------------------------------------------------------------- 1 | 23 | 24 | 25 | 26 | 44 | {/if} 45 | {#if schema.inputValueType === "date"} 46 | 47 | {/if} 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /src/lib/components/pages/Playground/PlaygroundSecondaryFormatters.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 | {#if secondaryFormatters.length} 20 |

{m.secondaryFormatters()}

21 | 22 | {#each secondaryFormatters as formatter, index} 23 | 24 | 30 | 31 | 32 | 33 | 34 | 35 | {/each} 36 | {/if} 37 | -------------------------------------------------------------------------------- /src/lib/components/pages/RelativeTimeFormat.svelte: -------------------------------------------------------------------------------- 1 | 47 | 48 | 49 | {#snippet input()} 50 | 51 |
52 | 53 | 54 | 55 |
56 |
57 | 58 | 59 |
60 | {/snippet} 61 | {#snippet output()} 62 | 63 | 64 | {#each relativeTimeFormatUnits as value} 65 | {#if value !== undefined} 66 | 67 | 80 | {/if} 81 | {/each} 82 | 83 | 84 | {/snippet} 85 |
86 | -------------------------------------------------------------------------------- /src/lib/components/pages/Segmenter.svelte: -------------------------------------------------------------------------------- 1 | 40 | 41 | 42 | {#snippet input()} 43 | 44 | {/snippet} 45 | {#snippet output()} 46 | 47 | {#each segmenterOptionsArray as [option, values], index} 48 | 53 | {#each values as value} 54 | {#if value !== undefined} 55 | 56 | 61 | {/if} 62 | {/each} 63 | 64 | {/each} 65 | 66 | {/snippet} 67 | 68 | -------------------------------------------------------------------------------- /src/lib/components/ui/BrowserSupport/BrowserSupport.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 | {#if data && $settings.showBrowserSupport} 21 |
22 |
23 |
24 | {#snippet summary()} 25 |
26 |

{m.browserSupport()}

27 | 28 |
29 | {/snippet} 30 | 31 |
32 |
33 |
34 | {:else if !data && $settings.showBrowserSupport} 35 | 36 | {/if} 37 | 38 | 56 | -------------------------------------------------------------------------------- /src/lib/components/ui/BrowserSupport/BrowserSupportGrid.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 | {#if data} 20 |
21 | {#each Object.entries(data) as [browserName, browserData]} 22 |
23 |
24 | 34 | {getAriaLabel(browserData.browserName, browserData.versionAdded)} 35 | {browserData.browserName} 36 |
37 | 40 |
41 | {/each} 42 |
43 | {/if} 44 | 45 | 63 | -------------------------------------------------------------------------------- /src/lib/components/ui/BrowserSupport/SpecificationUrls.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 |
13 | {#each getSpecUrls(data) as url} 14 | {m.specification()} 15 | {/each} 16 |
17 | -------------------------------------------------------------------------------- /src/lib/components/ui/BrowserSupport/SupportLabel.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |

14 | {#if support === "full" && !hideFullSupport} 15 | {m.fullSupport()} 16 | {:else if support === "none"} 17 | {m.noSupport()} 18 | {:else if support === "partial"} 19 | {m.partialSupport()} 20 | {/if} 21 |

22 | 23 | 37 | -------------------------------------------------------------------------------- /src/lib/components/ui/Button.svelte: -------------------------------------------------------------------------------- 1 | 49 | 50 | {#if href} 51 | 62 | {@render children?.()} 63 | 64 | {:else} 65 | 84 | {/if} 85 | 86 | 119 | -------------------------------------------------------------------------------- /src/lib/components/ui/Card.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 |
10 | {@render children?.()} 11 |
12 | 13 | 21 | -------------------------------------------------------------------------------- /src/lib/components/ui/Checkbox.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 |
17 | 18 | 19 |
20 | 21 | 36 | -------------------------------------------------------------------------------- /src/lib/components/ui/Chip.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
15 |

{label}

16 | {#if onDelete} 17 | 20 | {/if} 21 |
22 | 23 | 48 | -------------------------------------------------------------------------------- /src/lib/components/ui/CodeBlock.svelte: -------------------------------------------------------------------------------- 1 | 10 |
{@render children?.()}
11 | 12 | 13 | 27 | -------------------------------------------------------------------------------- /src/lib/components/ui/DateTime.svelte: -------------------------------------------------------------------------------- 1 | 27 | 28 |
29 |
30 | 31 | 32 | 33 |
34 |
35 | 36 | 37 | 38 |
39 |
40 | 41 | 54 | -------------------------------------------------------------------------------- /src/lib/components/ui/Dialog.svelte: -------------------------------------------------------------------------------- 1 | 23 | 24 | 25 | (show = false)} onclick={self(() => dialog?.close())}> 26 | 27 |
28 | 29 |

{header}

30 | 31 | {@render children?.()} 32 | 33 |
34 |
35 | 36 | 79 | -------------------------------------------------------------------------------- /src/lib/components/ui/DisplayNamesHighlight.svelte: -------------------------------------------------------------------------------- 1 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/lib/components/ui/Fieldset.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 |
20 | {legend} 21 | {@render children?.()} 22 |
23 | 24 | 40 | -------------------------------------------------------------------------------- /src/lib/components/ui/Grid.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 |
10 | {@render children?.()} 11 |
12 | 13 | 27 | -------------------------------------------------------------------------------- /src/lib/components/ui/Header.svelte: -------------------------------------------------------------------------------- 1 | 25 | 26 |
27 |

28 | {formatHeader(header)} 29 | {#if header === "DurationFormat" || link === "DurationFormat"} 30 | Experimental 31 | {/if} 32 |

33 | 39 |
40 | 41 | 42 | 61 | -------------------------------------------------------------------------------- /src/lib/components/ui/Highlight.svelte: -------------------------------------------------------------------------------- 1 | 28 | 29 |
30 |
{#each Object.entries(values) as [key, value], i}{"\t"}{#if Object.keys(values).length > 1 && i < Object.keys(values).length - 1}
{/if}{/each}

31 |
32 | 35 |
36 | 37 |
38 | 39 | 45 | -------------------------------------------------------------------------------- /src/lib/components/ui/Highlight/HighlightLocale.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | {#if locales.length === 1} 12 | 13 | {:else if !locales.length} 14 | 15 | {:else} 16 | `"${l}"`).join(",")} t="string" /> 20 | {/if} 21 | -------------------------------------------------------------------------------- /src/lib/components/ui/Highlight/Token.svelte: -------------------------------------------------------------------------------- 1 | 26 | {noTrim ? v : v.toString().trim()} 27 | 67 | -------------------------------------------------------------------------------- /src/lib/components/ui/HighlightValue.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/lib/components/ui/Input.svelte: -------------------------------------------------------------------------------- 1 | 28 | 29 |
30 | {#if label} 31 | 32 | {/if} 33 | 34 | {#if onInput} 35 | 46 | {:else} 47 | 48 | {/if} 49 |
50 | 51 | 56 | -------------------------------------------------------------------------------- /src/lib/components/ui/LocalePicker.svelte: -------------------------------------------------------------------------------- 1 | 22 | 23 | ({ value: v, label: v }))} 27 | {onSelect} 28 | {onDelete} 29 | options={languageByLocaleAsComboBoxOptions} 30 | /> 31 | -------------------------------------------------------------------------------- /src/lib/components/ui/MDNLink.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | MDN 18 | -------------------------------------------------------------------------------- /src/lib/components/ui/Main.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 |
11 |
12 | {@render children?.()} 13 |
14 |
15 | 16 | 29 | -------------------------------------------------------------------------------- /src/lib/components/ui/OptionCard.svelte: -------------------------------------------------------------------------------- 1 | 27 | 28 | 29 |
30 |
31 | 39 | 40 |
41 | 42 |
43 | 44 |
45 | {@render children?.()} 46 |
47 |
48 | 49 | 66 | -------------------------------------------------------------------------------- /src/lib/components/ui/OptionHeader.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 |

{text}

10 | 11 | 18 | -------------------------------------------------------------------------------- /src/lib/components/ui/OptionSection.svelte: -------------------------------------------------------------------------------- 1 | 26 | 27 |
28 |
29 | {#if labelId} 30 | 31 | {:else} 32 |

{header}

33 | {/if} 34 | {#if support?.support} 35 | 36 | {/if} 37 |
38 | 39 | {@render children?.()} 40 | 41 |
42 | 43 | 57 | -------------------------------------------------------------------------------- /src/lib/components/ui/ProgressBar.svelte: -------------------------------------------------------------------------------- 1 |
2 | 3 | 34 | -------------------------------------------------------------------------------- /src/lib/components/ui/Radio.svelte: -------------------------------------------------------------------------------- 1 | 22 | 23 | 27 | 28 | 103 | -------------------------------------------------------------------------------- /src/lib/components/ui/Select.svelte: -------------------------------------------------------------------------------- 1 | 35 | 36 |
37 | {#if label} 38 | 39 | {/if} 40 | 41 | 51 |
52 | 53 | 58 | -------------------------------------------------------------------------------- /src/lib/components/ui/Skeleton.svelte: -------------------------------------------------------------------------------- 1 |
2 | 3 | 38 | -------------------------------------------------------------------------------- /src/lib/components/ui/SkipLink.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 23 | -------------------------------------------------------------------------------- /src/lib/components/ui/Slider.svelte: -------------------------------------------------------------------------------- 1 | 43 | 44 |
45 | 46 | 47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | 55 | 89 | -------------------------------------------------------------------------------- /src/lib/components/ui/Spacing.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 |
10 | 11 | 49 | -------------------------------------------------------------------------------- /src/lib/components/ui/SrOnly.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | {@render children?.()} 11 | 12 | 13 | 28 | -------------------------------------------------------------------------------- /src/lib/components/ui/details/Details.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 |
16 | 17 | 18 | {@render summary?.()} 19 | 20 | 21 | 22 |
23 | {@render children?.()} 24 |
25 |
26 | 27 | 80 | -------------------------------------------------------------------------------- /src/lib/components/ui/icons/Add.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | -------------------------------------------------------------------------------- /src/lib/components/ui/icons/AndroidWebView.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 14 | -------------------------------------------------------------------------------- /src/lib/components/ui/icons/Browser.svelte: -------------------------------------------------------------------------------- 1 | 28 | -------------------------------------------------------------------------------- /src/lib/components/ui/icons/BrowserType.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | {#if browserType === "desktop"} 14 | 15 | {/if} 16 | {#if browserType === "mobile"} 17 | 18 | {/if} 19 | {#if browserType === "server"} 20 | 21 | {/if} 22 | -------------------------------------------------------------------------------- /src/lib/components/ui/icons/Check.svelte: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /src/lib/components/ui/icons/ChevronUp.svelte: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /src/lib/components/ui/icons/CopyToClipboard.svelte: -------------------------------------------------------------------------------- 1 | 22 | -------------------------------------------------------------------------------- /src/lib/components/ui/icons/Desktop.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 18 | 19 | 23 | 24 | 25 | 31 | -------------------------------------------------------------------------------- /src/lib/components/ui/icons/Edge.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /src/lib/components/ui/icons/Edit.svelte: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /src/lib/components/ui/icons/Firefox.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 14 | -------------------------------------------------------------------------------- /src/lib/components/ui/icons/Icon.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 | {#if browserName.includes("chrome")} 19 | 20 | {/if} 21 | {#if browserName.includes("firefox")} 22 | 23 | {/if} 24 | {#if browserName.includes("safari")} 25 | 26 | {/if} 27 | {#if browserName.includes("opera")} 28 | 29 | {/if} 30 | {#if browserName.includes("edge")} 31 | 32 | {/if} 33 | {#if browserName.includes("deno")} 34 | 35 | {/if} 36 | {#if browserName.includes("webview")} 37 | 38 | {/if} 39 | {#if browserName.includes("node")} 40 | 41 | {/if} 42 | {#if browserName.includes("samsung")} 43 | 44 | {/if} 45 | -------------------------------------------------------------------------------- /src/lib/components/ui/icons/Mobile.svelte: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 14 | -------------------------------------------------------------------------------- /src/lib/components/ui/icons/Moon.svelte: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /src/lib/components/ui/icons/Node.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 14 | -------------------------------------------------------------------------------- /src/lib/components/ui/icons/OpenInNewTab.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 19 | -------------------------------------------------------------------------------- /src/lib/components/ui/icons/Opera.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 14 | -------------------------------------------------------------------------------- /src/lib/components/ui/icons/Safari.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 14 | -------------------------------------------------------------------------------- /src/lib/components/ui/icons/Samsung.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 14 | -------------------------------------------------------------------------------- /src/lib/components/ui/icons/Server.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 21 | -------------------------------------------------------------------------------- /src/lib/components/ui/icons/Settings.svelte: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /src/lib/components/ui/icons/Sun.svelte: -------------------------------------------------------------------------------- 1 | 29 | -------------------------------------------------------------------------------- /src/lib/components/ui/icons/Times.svelte: -------------------------------------------------------------------------------- 1 | 18 | -------------------------------------------------------------------------------- /src/lib/components/ui/icons/chrome.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 14 | -------------------------------------------------------------------------------- /src/lib/constants.ts: -------------------------------------------------------------------------------- 1 | export const description = 2 | "Intl Explorer is an interactive tool for experimenting and trying out the ECMAScript Internationalization API."; 3 | export const title = "Intl Explorer"; 4 | export const imageUrl = "/static/social.png"; 5 | export const author = "Jesper Orb"; 6 | export const tags = ["Inlt", "i18n", "Internationalization", "JavaScript", "TypeScript", "Svelte"]; 7 | -------------------------------------------------------------------------------- /src/lib/format-methods.ts: -------------------------------------------------------------------------------- 1 | export const formatMethods = [ 2 | "NumberFormat", 3 | "DateTimeFormat", 4 | "RelativeTimeFormat", 5 | "ListFormat", 6 | "PluralRules", 7 | "Collator", 8 | "Segmenter", 9 | "DisplayNames", 10 | "DurationFormat" 11 | ] as const; 12 | 13 | export type FormatMethodsKeys = 14 | | "DateTimeFormat" 15 | | "NumberFormat" 16 | | "ListFormat" 17 | | "RelativeTimeFormat" 18 | | "PluralRules" 19 | | "Collator" 20 | | "Segmenter" 21 | | "DisplayNames" 22 | | "DurationFormat"; 23 | -------------------------------------------------------------------------------- /src/lib/format-options/collator.options.ts: -------------------------------------------------------------------------------- 1 | import { localeMatcher } from "$lib/format-options/common.options"; 2 | import type { CreateOptions } from "$types/common"; 3 | 4 | export type CollatorOptions = CreateOptions; 5 | 6 | export const collatorFormatOptions: CollatorOptions = { 7 | usage: ["sort", "search", undefined], 8 | numeric: [true, false, undefined], 9 | caseFirst: ["upper", "lower", "false", undefined], 10 | sensitivity: ["base", "accent", "case", "variant", undefined], 11 | ignorePunctuation: [true, false, undefined], 12 | localeMatcher 13 | }; 14 | 15 | export const collatorFormatOptionsArray = Object.entries(collatorFormatOptions); 16 | -------------------------------------------------------------------------------- /src/lib/format-options/common.options.ts: -------------------------------------------------------------------------------- 1 | import type { Tuple } from "$types/common"; 2 | 3 | export const style: Tuple = [ 4 | undefined, 5 | "long", 6 | "short", 7 | "narrow" 8 | ]; 9 | export const localeMatcher: Tuple = [ 10 | undefined, 11 | "best fit", 12 | "lookup" 13 | ]; 14 | export const roundingPriority: Tuple = [ 15 | "auto", 16 | "morePrecision", 17 | "lessPrecision", 18 | undefined 19 | ]; 20 | export const roundingIncrement: Tuple = [ 21 | 1, 22 | 2, 23 | 5, 24 | 10, 25 | 20, 26 | 25, 27 | 50, 28 | 100, 29 | 200, 30 | 250, 31 | 500, 32 | 1000, 33 | 2000, 34 | 2500, 35 | 5000, 36 | undefined 37 | ]; 38 | export const roundingMode: Tuple = [ 39 | "ceil", 40 | "floor", 41 | "expand", 42 | "trunc", 43 | "halfCeil", 44 | "halfFloor", 45 | "halfExpand", 46 | "halfTrunc", 47 | "halfEven", 48 | undefined 49 | ]; 50 | export const trailingZeroDisplay: Tuple = [ 51 | "auto", 52 | "stripIfInteger", 53 | undefined 54 | ]; 55 | -------------------------------------------------------------------------------- /src/lib/format-options/datetime-format.options.ts: -------------------------------------------------------------------------------- 1 | import { calendars, numberingSystems } from "$lib/locale-data/calendars"; 2 | import type { AllFormatOptionsKeys } from "$lib/format-options/types"; 3 | import { style, localeMatcher } from "$lib/format-options/common.options"; 4 | import type { CreateOptions, StringOrUndefinedTuple } from "$types/common"; 5 | 6 | export type DateTimeFormatOptions = CreateOptions; 7 | 8 | export const datetimeFormatOptions: DateTimeFormatOptions = { 9 | dateStyle: ["full", "long", "medium", "short", undefined], 10 | timeStyle: ["full", "long", "medium", "short", undefined], 11 | year: ["numeric", "2-digit", undefined], 12 | month: ["numeric", "2-digit", ...style], 13 | day: ["numeric", "2-digit", undefined], 14 | hour: ["numeric", "2-digit", undefined], 15 | minute: ["numeric", "2-digit", undefined], 16 | second: ["numeric", "2-digit", undefined], 17 | weekday: style, 18 | era: style, 19 | hour12: [true, false, undefined], 20 | hourCycle: ["h11", "h12", "h23", "h24", undefined], 21 | dayPeriod: style, 22 | fractionalSecondDigits: [1, 2, 3, undefined], 23 | calendar: [...calendars, undefined] as StringOrUndefinedTuple, 24 | numberingSystem: [...numberingSystems, undefined] as StringOrUndefinedTuple, 25 | timeZoneName: [ 26 | "long", 27 | "short", 28 | "shortOffset", 29 | "longOffset", 30 | "shortGeneric", 31 | "longGeneric", 32 | undefined 33 | ], 34 | formatMatcher: ["best fit", "basic", undefined], 35 | localeMatcher 36 | }; 37 | 38 | export const dateTimeFormatOptionsArray = Object.entries(datetimeFormatOptions); 39 | 40 | export const getDateTimeFormatOptions = ( 41 | option: AllFormatOptionsKeys["DateTimeFormat"] | string, 42 | value: string | boolean | number 43 | ): Partial => { 44 | if (option === "fractionalSecondDigits") { 45 | return { 46 | [option as AllFormatOptionsKeys["DateTimeFormat"]]: value as number, 47 | second: "numeric" 48 | }; 49 | } 50 | if (option === "hour12" || option == "hourCycle") { 51 | return { 52 | [option as AllFormatOptionsKeys["DateTimeFormat"]]: value as number, 53 | timeStyle: "medium" 54 | }; 55 | } 56 | return { 57 | [option]: value 58 | }; 59 | }; 60 | -------------------------------------------------------------------------------- /src/lib/format-options/display-names.options.ts: -------------------------------------------------------------------------------- 1 | import { style, localeMatcher } from "$lib/format-options/common.options"; 2 | import type { CreateOptions } from "$types/common"; 3 | 4 | export type DateTimeFormatOptions = CreateOptions; 5 | 6 | const type: (Intl.DisplayNamesType | undefined)[] = [ 7 | "language", 8 | "region", 9 | "script", 10 | "calendar", 11 | "dateTimeField", 12 | "currency", 13 | undefined 14 | ]; 15 | 16 | export const displayNamesOptions: DateTimeFormatOptions = { 17 | style, 18 | type: type as [ 19 | Intl.DisplayNamesType, 20 | Intl.DisplayNamesType, 21 | Intl.DisplayNamesType, 22 | Intl.DisplayNamesType, 23 | Intl.DisplayNamesType, 24 | Intl.DisplayNamesType 25 | ], 26 | languageDisplay: ["dialect", "standard", undefined], 27 | fallback: ["code", "none", undefined], 28 | localeMatcher 29 | }; 30 | -------------------------------------------------------------------------------- /src/lib/format-options/duration-format.options.ts: -------------------------------------------------------------------------------- 1 | import { defaultNumberRange } from "$utils/format-utils"; 2 | import { style, localeMatcher } from "$lib/format-options/common.options"; 3 | import type { CreateOptions, NumberOrUndefinedTuple } from "$types/common"; 4 | 5 | type DurationFormatOptions = CreateOptions; 6 | 7 | export const durationFormatOptions: DurationFormatOptions = { 8 | style: [...style, "digital"], 9 | years: style, 10 | months: style, 11 | weeks: style, 12 | days: style, 13 | hours: style, 14 | minutes: style, 15 | seconds: style, 16 | milliseconds: style, 17 | microseconds: style, 18 | nanoseconds: style, 19 | fractionalDigits: [...defaultNumberRange, undefined] as NumberOrUndefinedTuple, 20 | localeMatcher 21 | }; 22 | 23 | export const durationFormatOptionsArray = Object.entries(durationFormatOptions); 24 | 25 | export const durationValues: (keyof Intl.Duration)[] = [ 26 | "years", 27 | "months", 28 | "weeks", 29 | "days", 30 | "hours", 31 | "minutes", 32 | "seconds", 33 | "milliseconds", 34 | "microseconds", 35 | "nanoseconds" 36 | ]; 37 | -------------------------------------------------------------------------------- /src/lib/format-options/index.ts: -------------------------------------------------------------------------------- 1 | import { collatorFormatOptions } from "$lib/format-options/collator.options"; 2 | import { datetimeFormatOptions } from "$lib/format-options/datetime-format.options"; 3 | import { displayNamesOptions } from "$lib/format-options/display-names.options"; 4 | import { durationFormatOptions } from "$lib/format-options/duration-format.options"; 5 | import { listFormatOptions } from "$lib/format-options/list-format.options"; 6 | import { 7 | numberFormatOptionsCommon, 8 | numberFormatOptionsCurrency, 9 | numberFormatOptionsUnit 10 | } from "$lib/format-options/number-format.options"; 11 | import { pluralRulesFormatOptions } from "$lib/format-options/plural-rules.options"; 12 | import { relativeTimeFormatOptions } from "$lib/format-options/relative-time-format.options"; 13 | import { segmenterOptions } from "$lib/format-options/segmenter.options"; 14 | 15 | export const formatOptions = { 16 | DateTimeFormat: datetimeFormatOptions, 17 | NumberFormat: { 18 | ...numberFormatOptionsUnit, 19 | ...numberFormatOptionsCurrency, 20 | ...numberFormatOptionsCommon 21 | }, 22 | ListFormat: listFormatOptions, 23 | RelativeTimeFormat: relativeTimeFormatOptions, 24 | PluralRules: pluralRulesFormatOptions, 25 | Collator: collatorFormatOptions, 26 | Segmenter: segmenterOptions, 27 | DisplayNames: displayNamesOptions, 28 | DurationFormat: durationFormatOptions 29 | }; 30 | -------------------------------------------------------------------------------- /src/lib/format-options/list-format.options.ts: -------------------------------------------------------------------------------- 1 | import { style, localeMatcher } from "$lib/format-options/common.options"; 2 | import type { CreateOptions } from "$types/common"; 3 | 4 | export type ListFormatOptions = CreateOptions; 5 | 6 | export const listFormatOptions: ListFormatOptions = { 7 | type: [undefined, "conjunction", "disjunction", "unit"], 8 | style, 9 | localeMatcher 10 | }; 11 | 12 | export const listFormatOptionsArray = Object.entries(listFormatOptions); 13 | -------------------------------------------------------------------------------- /src/lib/format-options/number-format.options.ts: -------------------------------------------------------------------------------- 1 | import { currencies } from "$lib/locale-data/currencies"; 2 | import { units } from "$lib/locale-data/units"; 3 | import { defaultNumberRange } from "$utils/format-utils"; 4 | import { 5 | localeMatcher, 6 | roundingIncrement, 7 | roundingMode, 8 | roundingPriority, 9 | trailingZeroDisplay, 10 | style 11 | } from "$lib/format-options/common.options"; 12 | import type { CreateOptions } from "$types/common"; 13 | 14 | export type NumberFormatOptions = CreateOptions; 15 | 16 | export const numberFormatOptionsCommon = { 17 | style: ["currency", "unit"], 18 | signDisplay: ["auto", "never", "always", "exceptZero", undefined], 19 | useGrouping: [true, false, undefined], 20 | minimumIntegerDigits: [...defaultNumberRange, undefined], 21 | minimumFractionDigits: [...defaultNumberRange, undefined], 22 | maximumFractionDigits: [...defaultNumberRange, undefined], 23 | minimumSignificantDigits: [...defaultNumberRange, undefined], 24 | maximumSignificantDigits: [...defaultNumberRange, undefined], 25 | roundingIncrement, 26 | roundingMode, 27 | roundingPriority, 28 | trailingZeroDisplay, 29 | localeMatcher 30 | }; 31 | 32 | export const numberFormatOptionsCurrency: NumberFormatOptions = { 33 | currencySign: ["standard", "accounting", undefined], 34 | currencyDisplay: ["code", "symbol", "narrowSymbol", "name", undefined], 35 | currency: Object.keys(currencies) as [string | undefined, string | undefined] 36 | }; 37 | 38 | export const numberFormatOptionsUnit: NumberFormatOptions = { 39 | unit: units as [string | undefined, string | undefined], 40 | unitDisplay: style, 41 | compactDisplay: ["short", "long", undefined], 42 | notation: ["standard", "scientific", "engineering", "compact", undefined] 43 | }; 44 | -------------------------------------------------------------------------------- /src/lib/format-options/plural-rules.options.ts: -------------------------------------------------------------------------------- 1 | import { defaultNumberRange } from "$utils/format-utils"; 2 | import { 3 | localeMatcher, 4 | roundingIncrement, 5 | roundingMode, 6 | roundingPriority, 7 | trailingZeroDisplay 8 | } from "$lib/format-options/common.options"; 9 | import type { CreateOptions, NumberOrUndefinedTuple } from "$types/common"; 10 | 11 | export type PluralRulesOptions = CreateOptions; 12 | 13 | export const pluralRulesFormatOptions: PluralRulesOptions = { 14 | type: ["cardinal", "ordinal", undefined], 15 | minimumIntegerDigits: [...defaultNumberRange, undefined] as NumberOrUndefinedTuple, 16 | minimumFractionDigits: [...defaultNumberRange, undefined] as NumberOrUndefinedTuple, 17 | maximumFractionDigits: [...defaultNumberRange, undefined] as NumberOrUndefinedTuple, 18 | minimumSignificantDigits: [...defaultNumberRange, undefined] as NumberOrUndefinedTuple, 19 | maximumSignificantDigits: [...defaultNumberRange, undefined] as NumberOrUndefinedTuple, 20 | roundingIncrement, 21 | roundingMode, 22 | roundingPriority, 23 | trailingZeroDisplay, 24 | localeMatcher 25 | }; 26 | -------------------------------------------------------------------------------- /src/lib/format-options/relative-time-format.options.ts: -------------------------------------------------------------------------------- 1 | import { localeMatcher, style } from "$lib/format-options/common.options"; 2 | import type { CreateOptions } from "$types/common"; 3 | 4 | export type RelativeTimeFormatOptions = CreateOptions & { 5 | unit: Intl.RelativeTimeFormatUnit[]; 6 | }; 7 | 8 | export const relativeTimeFormatUnits: Intl.RelativeTimeFormatUnit[] = [ 9 | "year", 10 | "years", 11 | "quarter", 12 | "quarters", 13 | "month", 14 | "months", 15 | "week", 16 | "weeks", 17 | "day", 18 | "days", 19 | "hour", 20 | "hours", 21 | "minute", 22 | "minutes", 23 | "second", 24 | "seconds" 25 | ]; 26 | 27 | export const relativeTimeFormatOptions: RelativeTimeFormatOptions = { 28 | numeric: ["always", "auto", undefined], 29 | style, 30 | unit: relativeTimeFormatUnits, 31 | localeMatcher 32 | }; 33 | -------------------------------------------------------------------------------- /src/lib/format-options/segmenter.options.ts: -------------------------------------------------------------------------------- 1 | import { localeMatcher } from "$lib/format-options/common.options"; 2 | import type { CreateOptions } from "$types/common"; 3 | 4 | export type SegmenterOptions = CreateOptions; 5 | 6 | export const segmenterOptions: SegmenterOptions = { 7 | granularity: ["word", "sentence", "grapheme", undefined], 8 | localeMatcher 9 | }; 10 | 11 | export const segmenterOptionsArray = Object.entries(segmenterOptions); 12 | -------------------------------------------------------------------------------- /src/lib/format-options/types.ts: -------------------------------------------------------------------------------- 1 | export type AllFormatOptions = { 2 | DateTimeFormat: Intl.DateTimeFormatOptions; 3 | NumberFormat: Intl.NumberFormatOptions; 4 | ListFormat: Intl.ListFormatOptions; 5 | RelativeTimeFormat: Intl.RelativeTimeFormatOptions; 6 | PluralRules: Intl.PluralRulesOptions; 7 | Collator: Intl.CollatorOptions; 8 | Segmenter: Intl.SegmenterOptions; 9 | DisplayNames: Intl.DisplayNamesOptions; 10 | DurationFormat: Intl.DurationFormatOptions; 11 | }; 12 | 13 | export type AllFormatOptionsKeys = { 14 | DateTimeFormat: keyof AllFormatOptions["DateTimeFormat"]; 15 | NumberFormat: keyof AllFormatOptions["NumberFormat"]; 16 | ListFormat: keyof AllFormatOptions["ListFormat"]; 17 | RelativeTimeFormat: keyof AllFormatOptions["RelativeTimeFormat"]; 18 | PluralRules: keyof AllFormatOptions["PluralRules"]; 19 | Collator: keyof AllFormatOptions["Collator"]; 20 | Segmenter: keyof AllFormatOptions["Segmenter"]; 21 | DisplayNames: keyof AllFormatOptions["DisplayNames"]; 22 | DurationFormat: keyof AllFormatOptions["DurationFormat"]; 23 | }; 24 | 25 | export type AllMethods = { 26 | DateTimeFormat: Intl.DateTimeFormat; 27 | NumberFormat: Intl.NumberFormat; 28 | ListFormat: Intl.ListFormat; 29 | RelativeTimeFormat: Intl.RelativeTimeFormat; 30 | PluralRules: Intl.PluralRules; 31 | Collator: Intl.Collator; 32 | Segmenter: Intl.Segmenter; 33 | DisplayNames: Intl.DisplayNames; 34 | DurationFormat: Intl.DurationFormat; 35 | }; 36 | -------------------------------------------------------------------------------- /src/lib/live-announcer/constants.ts: -------------------------------------------------------------------------------- 1 | import type { AnnounceOptions } from "./types"; 2 | 3 | export const liveAnnouncerContextKey = "live-announcer"; 4 | export const liveAnnouncerRegionIdPolite = "live-announcer-polite"; 5 | export const liveAnnouncerRegionIdAssertive = "live-announcer-assertive"; 6 | export const defaultAnnounceTimeoutMs = 1000; 7 | export const defaultAnnounceSetting: AnnounceOptions["setting"] = "polite"; 8 | export const defaultAnnounceOptions: AnnounceOptions = { 9 | setting: defaultAnnounceSetting, 10 | timeout: defaultAnnounceTimeoutMs 11 | }; 12 | -------------------------------------------------------------------------------- /src/lib/live-announcer/live-announcer-region.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 |
9 |
10 | -------------------------------------------------------------------------------- /src/lib/live-announcer/live-announcer.svelte: -------------------------------------------------------------------------------- 1 | 37 | 38 | 39 | 40 | 41 | {@render children?.()} 42 | -------------------------------------------------------------------------------- /src/lib/live-announcer/types.ts: -------------------------------------------------------------------------------- 1 | import type { AriaAttributes } from "svelte/elements"; 2 | 3 | export type AnnounceOptions = { 4 | timeout?: number; 5 | setting: NonNullable; 6 | }; 7 | 8 | export type AnnounceFunction = (message: string, options?: AnnounceOptions) => void; 9 | -------------------------------------------------------------------------------- /src/lib/live-announcer/util.ts: -------------------------------------------------------------------------------- 1 | import { getContext } from "svelte"; 2 | import type { AnnounceFunction } from "./types"; 3 | import { liveAnnouncerContextKey } from "./constants"; 4 | 5 | export const getAnnouncer = () => getContext(liveAnnouncerContextKey) as AnnounceFunction; 6 | -------------------------------------------------------------------------------- /src/lib/locale-data/calendars.ts: -------------------------------------------------------------------------------- 1 | export const calendars = [ 2 | "buddhist", 3 | "chinese", 4 | "coptic", 5 | "ethiopia", 6 | "ethiopic", 7 | "gregory", 8 | "hebrew", 9 | "indian", 10 | "islamic", 11 | "iso8601", 12 | "japanese", 13 | "persian", 14 | "roc" 15 | ]; 16 | 17 | export const numberingSystems = [ 18 | "arab", 19 | "arabext", 20 | "bali", 21 | "beng", 22 | "deva", 23 | "fullwide", 24 | "gujr", 25 | "guru", 26 | "hanidec", 27 | "khmr", 28 | "knda", 29 | "laoo", 30 | "latn", 31 | "limb", 32 | "mlym", 33 | "mong", 34 | "mymr", 35 | "orya", 36 | "tamldec", 37 | "telu", 38 | "thai", 39 | "tibt" 40 | ]; 41 | -------------------------------------------------------------------------------- /src/lib/locale-data/units.ts: -------------------------------------------------------------------------------- 1 | export const units = [ 2 | "acre", 3 | "bit", 4 | "byte", 5 | "celsius", 6 | "centimeter", 7 | "day", 8 | "degree", 9 | "fahrenheit", 10 | "fluid-ounce", 11 | "foot", 12 | "gallon", 13 | "gigabit", 14 | "gigabyte", 15 | "gram", 16 | "hectare", 17 | "hour", 18 | "inch", 19 | "kilobit", 20 | "kilobyte", 21 | "kilogram", 22 | "kilometer", 23 | "liter", 24 | "megabit", 25 | "megabyte", 26 | "meter", 27 | "mile", 28 | "mile-scandinavian", 29 | "milliliter", 30 | "millimeter", 31 | "millisecond", 32 | "minute", 33 | "month", 34 | "ounce", 35 | "percent", 36 | "petabyte", 37 | "pound", 38 | "second", 39 | "stone", 40 | "terabit", 41 | "terabyte", 42 | "week", 43 | "yard", 44 | "year" 45 | ]; 46 | 47 | export const unitsAsEntries = units.map((unit) => [unit, unit]); 48 | -------------------------------------------------------------------------------- /src/lib/playground/playground.schema.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import type { FormatMethodsKeys } from "$lib/format-methods"; 3 | import type { AllFormatOptions, AllMethods } from "$lib/format-options/types"; 4 | 5 | export type InputType = "select" | "text" | "radio"; 6 | export type ValueType = "string" | "number" | "boolean"; 7 | export type InputValueType = "array" | "date" | "number" | "string"; 8 | 9 | export type PlaygroundOption< 10 | Method extends FormatMethodsKeys, 11 | Options extends AllFormatOptions[Method] = AllFormatOptions[Method], 12 | Key extends keyof Options = keyof Options & string 13 | > = { 14 | name: Key; 15 | valueType: ValueType; 16 | inputType: InputType; 17 | defaultValue: Options[Key]; 18 | removeUndefined?: boolean; 19 | value?: Options[Key]; 20 | pattern?: string; 21 | max?: number; 22 | min?: number; 23 | selected?: boolean; 24 | }; 25 | 26 | export type PlaygroundSchema< 27 | Method extends FormatMethodsKeys, 28 | Formatter = keyof AllMethods[Method] 29 | > = { 30 | method: Method; 31 | primaryFormatter: Formatter; 32 | secondaryFormatters?: Formatter[]; 33 | inputValues: any[]; 34 | inputValueType: InputValueType; 35 | options: PlaygroundOption[]; 36 | invalidOptionCombos?: { 37 | [path: string]: string[]; 38 | }; 39 | }; 40 | -------------------------------------------------------------------------------- /src/lib/playground/schemas/collator.schema.ts: -------------------------------------------------------------------------------- 1 | import type { PlaygroundSchema } from "$lib/playground/playground.schema"; 2 | 3 | export const collatorSchema: PlaygroundSchema<"Collator"> = { 4 | method: "Collator", 5 | primaryFormatter: "compare", 6 | inputValues: [["Z", "a", "z", "ä", "1", "=", "à"]], 7 | inputValueType: "array", 8 | options: [ 9 | { 10 | name: "usage", 11 | valueType: "string", 12 | defaultValue: undefined, 13 | inputType: "select" 14 | }, 15 | { 16 | name: "caseFirst", 17 | valueType: "string", 18 | defaultValue: undefined, 19 | inputType: "select" 20 | }, 21 | { 22 | name: "sensitivity", 23 | valueType: "string", 24 | defaultValue: undefined, 25 | inputType: "select" 26 | }, 27 | { 28 | name: "caseFirst", 29 | valueType: "string", 30 | defaultValue: undefined, 31 | inputType: "select" 32 | }, 33 | { 34 | name: "numeric", 35 | valueType: "boolean", 36 | defaultValue: undefined, 37 | inputType: "radio" 38 | }, 39 | { 40 | name: "ignorePunctuation", 41 | valueType: "boolean", 42 | defaultValue: undefined, 43 | inputType: "radio" 44 | }, 45 | { 46 | name: "localeMatcher", 47 | valueType: "string", 48 | defaultValue: undefined, 49 | inputType: "select" 50 | } 51 | ] 52 | }; 53 | -------------------------------------------------------------------------------- /src/lib/playground/schemas/displayNames.schema.ts: -------------------------------------------------------------------------------- 1 | import type { PlaygroundSchema } from "$lib/playground/playground.schema"; 2 | 3 | export const displayNamesSchema: PlaygroundSchema<"DisplayNames"> = { 4 | method: "DisplayNames", 5 | primaryFormatter: "of", 6 | inputValues: ["US"], 7 | inputValueType: "string", 8 | options: [ 9 | { 10 | name: "type", 11 | removeUndefined: true, 12 | valueType: "string", 13 | defaultValue: "region", 14 | inputType: "select" 15 | }, 16 | { 17 | name: "style", 18 | valueType: "string", 19 | defaultValue: undefined, 20 | inputType: "select" 21 | }, 22 | { 23 | name: "languageDisplay", 24 | valueType: "string", 25 | defaultValue: undefined, 26 | inputType: "select" 27 | }, 28 | { 29 | name: "fallback", 30 | valueType: "string", 31 | defaultValue: undefined, 32 | inputType: "select" 33 | }, 34 | { 35 | name: "localeMatcher", 36 | valueType: "string", 37 | defaultValue: undefined, 38 | inputType: "select" 39 | } 40 | ] 41 | }; 42 | -------------------------------------------------------------------------------- /src/lib/playground/schemas/durationFormat.schema.ts: -------------------------------------------------------------------------------- 1 | import { durationValues } from "$lib/format-options/duration-format.options"; 2 | import type { PlaygroundOption, PlaygroundSchema } from "$lib/playground/playground.schema"; 3 | 4 | export const durationFormatSchema: PlaygroundSchema<"DurationFormat"> = { 5 | method: "DurationFormat", 6 | primaryFormatter: "format", 7 | secondaryFormatters: ["formatToParts"], 8 | inputValues: [[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]], 9 | inputValueType: "array", 10 | options: [ 11 | { 12 | name: "style", 13 | valueType: "string", 14 | defaultValue: undefined, 15 | inputType: "select" 16 | }, 17 | ...(durationValues.map((duration) => ({ 18 | name: duration, 19 | valueType: "string", 20 | defaultValue: undefined, 21 | inputType: "select" 22 | })) as PlaygroundOption<"DurationFormat">[]), 23 | { 24 | name: "fractionalDigits", 25 | valueType: "number", 26 | defaultValue: undefined, 27 | inputType: "text", 28 | max: 3, 29 | min: 0 30 | } 31 | ] 32 | }; 33 | -------------------------------------------------------------------------------- /src/lib/playground/schemas/index.ts: -------------------------------------------------------------------------------- 1 | import { collatorSchema } from "$lib/playground/schemas/collator.schema"; 2 | import { dateTimeFormatSchema } from "$lib/playground/schemas/dateTimeFormat.schema"; 3 | import { displayNamesSchema } from "$lib/playground/schemas/displayNames.schema"; 4 | import { durationFormatSchema } from "$lib/playground/schemas/durationFormat.schema"; 5 | import { listFormatSchema } from "$lib/playground/schemas/listFormat.schema"; 6 | import { numberFormatSchema } from "$lib/playground/schemas/numberFormat.schema"; 7 | import { pluralRulesSchema } from "$lib/playground/schemas/pluralRules.schema"; 8 | import { relativeTimeFormatSchema } from "$lib/playground/schemas/relativeTimeFormat.schema"; 9 | import { segmenterSchema } from "$lib/playground/schemas/segmenter.schema"; 10 | 11 | export const schemas = { 12 | NumberFormat: numberFormatSchema, 13 | ListFormat: listFormatSchema, 14 | PluralRules: pluralRulesSchema, 15 | DateTimeFormat: dateTimeFormatSchema, 16 | RelativeTimeFormat: relativeTimeFormatSchema, 17 | Collator: collatorSchema, 18 | Segmenter: segmenterSchema, 19 | DisplayNames: displayNamesSchema, 20 | DurationFormat: durationFormatSchema 21 | }; 22 | 23 | export type SchemaKeys = keyof typeof schemas; 24 | -------------------------------------------------------------------------------- /src/lib/playground/schemas/listFormat.schema.ts: -------------------------------------------------------------------------------- 1 | import type { PlaygroundSchema } from "$lib/playground/playground.schema"; 2 | 3 | export const listFormatSchema: PlaygroundSchema<"ListFormat"> = { 4 | method: "ListFormat", 5 | primaryFormatter: "format", 6 | secondaryFormatters: ["formatToParts"], 7 | inputValues: [["cat", "hat", "rat"]], 8 | inputValueType: "array", 9 | options: [ 10 | { 11 | name: "type", 12 | defaultValue: undefined, 13 | inputType: "select", 14 | valueType: "string" 15 | }, 16 | { 17 | name: "style", 18 | defaultValue: undefined, 19 | valueType: "string", 20 | inputType: "select" 21 | } 22 | ] 23 | }; 24 | -------------------------------------------------------------------------------- /src/lib/playground/schemas/pluralRules.schema.ts: -------------------------------------------------------------------------------- 1 | import type { PlaygroundSchema } from "$lib/playground/playground.schema"; 2 | 3 | export const pluralRulesSchema: PlaygroundSchema<"PluralRules"> = { 4 | method: "PluralRules", 5 | primaryFormatter: "select", 6 | inputValues: [2, 10], 7 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 8 | secondaryFormatters: ["selectRange" as any], 9 | inputValueType: "number", 10 | options: [ 11 | { 12 | name: "type", 13 | valueType: "string", 14 | defaultValue: undefined, 15 | inputType: "select" 16 | }, 17 | { 18 | name: "minimumIntegerDigits", 19 | valueType: "number", 20 | defaultValue: undefined, 21 | inputType: "text", 22 | max: 21, 23 | min: 1 24 | }, 25 | { 26 | name: "minimumFractionDigits", 27 | valueType: "number", 28 | defaultValue: undefined, 29 | inputType: "text", 30 | max: 20, 31 | min: 1 32 | }, 33 | { 34 | name: "maximumFractionDigits", 35 | valueType: "number", 36 | defaultValue: undefined, 37 | inputType: "text", 38 | max: 20, 39 | min: 1 40 | }, 41 | { 42 | name: "minimumSignificantDigits", 43 | valueType: "number", 44 | defaultValue: undefined, 45 | inputType: "text", 46 | max: 21, 47 | min: 1 48 | }, 49 | { 50 | name: "maximumSignificantDigits", 51 | valueType: "number", 52 | defaultValue: undefined, 53 | inputType: "text", 54 | max: 21, 55 | min: 1 56 | }, 57 | { 58 | name: "roundingIncrement", 59 | valueType: "string", 60 | defaultValue: undefined, 61 | inputType: "select" 62 | }, 63 | { 64 | name: "roundingMode", 65 | valueType: "string", 66 | defaultValue: undefined, 67 | inputType: "select" 68 | }, 69 | { 70 | name: "roundingPriority", 71 | valueType: "string", 72 | defaultValue: undefined, 73 | inputType: "select" 74 | }, 75 | { 76 | name: "trailingZeroDisplay", 77 | valueType: "string", 78 | defaultValue: undefined, 79 | inputType: "select" 80 | } 81 | ] 82 | }; 83 | -------------------------------------------------------------------------------- /src/lib/playground/schemas/relativeTimeFormat.schema.ts: -------------------------------------------------------------------------------- 1 | import type { PlaygroundSchema } from "$lib/playground/playground.schema"; 2 | 3 | export const relativeTimeFormatSchema: PlaygroundSchema<"RelativeTimeFormat"> = { 4 | method: "RelativeTimeFormat", 5 | primaryFormatter: "format", 6 | inputValues: [2, "days"], 7 | inputValueType: "number", 8 | secondaryFormatters: ["formatToParts"], 9 | options: [ 10 | { 11 | name: "unit" as keyof Intl.RelativeTimeFormatOptions, 12 | valueType: "string", 13 | defaultValue: "day" as Intl.RelativeTimeFormatNumeric, 14 | inputType: "select", 15 | removeUndefined: true 16 | }, 17 | { 18 | name: "numeric", 19 | valueType: "string", 20 | defaultValue: undefined, 21 | inputType: "select" 22 | }, 23 | { 24 | name: "style", 25 | valueType: "string", 26 | defaultValue: undefined, 27 | inputType: "select" 28 | }, 29 | { 30 | name: "localeMatcher", 31 | valueType: "string", 32 | defaultValue: undefined, 33 | inputType: "select" 34 | } 35 | ] 36 | }; 37 | -------------------------------------------------------------------------------- /src/lib/playground/schemas/segmenter.schema.ts: -------------------------------------------------------------------------------- 1 | import type { PlaygroundSchema } from "$lib/playground/playground.schema"; 2 | 3 | export const segmenterSchema: PlaygroundSchema<"Segmenter"> = { 4 | method: "Segmenter", 5 | primaryFormatter: "segment", 6 | inputValues: ["A normal sentence."], 7 | inputValueType: "string", 8 | options: [ 9 | { 10 | name: "granularity", 11 | valueType: "string", 12 | defaultValue: undefined, 13 | inputType: "select" 14 | }, 15 | { 16 | name: "localeMatcher", 17 | valueType: "string", 18 | defaultValue: undefined, 19 | inputType: "select" 20 | } 21 | ] 22 | }; 23 | -------------------------------------------------------------------------------- /src/lib/playground/url.utils.ts: -------------------------------------------------------------------------------- 1 | import { browser } from "$app/environment"; 2 | import type { FormatMethodsKeys } from "$lib/format-methods"; 3 | import type { PlaygroundSchema } from "$lib/playground/playground.schema"; 4 | import { validateAndUpdateSchema } from "$lib/playground/validate"; 5 | 6 | export const getSchemaParam = () => 7 | browser ? new URLSearchParams(window.location.search).get("schema") : undefined; 8 | 9 | export const parseSchemaFromURL = (): 10 | | PlaygroundSchema 11 | | undefined => { 12 | const params = browser ? new URLSearchParams(window.location.search).get("schema") : undefined; 13 | const parsedParams = params 14 | ? (JSON.parse(atob(params)).schema as PlaygroundSchema) 15 | : undefined; 16 | return parsedParams ? validateAndUpdateSchema(parsedParams) : undefined; 17 | }; 18 | 19 | export const createSchemaUrl = ( 20 | schema: PlaygroundSchema 21 | ) => { 22 | const baseEncoded = btoa(JSON.stringify({ schema })); 23 | const params = new URLSearchParams(window.location.search); 24 | params.set("schema", baseEncoded); 25 | return `${window.location.origin}${window.location.pathname}?${params.toString()}`; 26 | }; 27 | -------------------------------------------------------------------------------- /src/lib/playground/validate.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import type { FormatMethodsKeys } from "$lib/format-methods"; 3 | import type { PlaygroundOption, PlaygroundSchema } from "$lib/playground/playground.schema"; 4 | import { schemas } from "$lib/playground/schemas"; 5 | 6 | export const optionIsActive = ( 7 | option: PlaygroundOption 8 | ) => { 9 | if (option.selected !== undefined) { 10 | return option.selected; 11 | } 12 | if (option.value !== undefined) { 13 | return true; 14 | } 15 | return option.defaultValue !== undefined; 16 | }; 17 | 18 | export const validateAndUpdateSchema = ( 19 | schema: PlaygroundSchema 20 | ): PlaygroundSchema => { 21 | const templateSchema = schemas[schema.method as keyof typeof schemas]; 22 | const templateSchemaOptions = [...templateSchema.options]; 23 | const mappedSchemaOptions = Object.fromEntries( 24 | schema.options.map((option) => [option.name, option]) 25 | ); 26 | let invalidOptions: string[] | undefined = undefined; 27 | const options: PlaygroundOption[] = []; 28 | for (const option of templateSchemaOptions) { 29 | const value = mappedSchemaOptions[option.name]?.value; 30 | const selected = mappedSchemaOptions[option.name]?.selected; 31 | invalidOptions = 32 | templateSchema.invalidOptionCombos && !invalidOptions 33 | ? templateSchema.invalidOptionCombos[`${option.name}:${value ?? option.defaultValue}`] 34 | : invalidOptions; 35 | options.push({ 36 | ...option, 37 | selected, 38 | value 39 | } as any); 40 | } 41 | const optionsWithoutInvalidOptions = invalidOptions 42 | ? options.filter((option) => invalidOptions && !invalidOptions.includes(option.name as string)) 43 | : options; 44 | // Special case for numberformat as the style property requires the unit property to be set 45 | const style = 46 | schema.method === "NumberFormat" 47 | ? optionsWithoutInvalidOptions.find((option) => option.name === "style") 48 | : undefined; 49 | if (style && style.value === "unit") { 50 | const unit = optionsWithoutInvalidOptions.findIndex((option) => option.name === "unit"); 51 | if (unit && !optionsWithoutInvalidOptions[unit].value) { 52 | optionsWithoutInvalidOptions[unit].value = "degree" as any; 53 | } 54 | } 55 | return { 56 | ...schema, 57 | options: optionsWithoutInvalidOptions as PlaygroundOption[] 58 | }; 59 | }; 60 | -------------------------------------------------------------------------------- /src/lib/routes.ts: -------------------------------------------------------------------------------- 1 | export type Route = { 2 | path: string; 3 | name: string; 4 | sublink?: boolean; 5 | ariaLabel?: string; 6 | experimental?: boolean; 7 | }; 8 | 9 | export const routes: Route[] = [ 10 | { 11 | path: "NumberFormat", 12 | name: "NumberFormat" 13 | }, 14 | { 15 | path: "NumberFormat/Currency", 16 | name: "Currency", 17 | sublink: true, 18 | ariaLabel: "NumberFormat Currency Style" 19 | }, 20 | { 21 | path: "NumberFormat/Unit", 22 | name: "Unit", 23 | sublink: true, 24 | ariaLabel: "NumberFormat Unit Style" 25 | }, 26 | { 27 | path: "DateTimeFormat", 28 | name: "DateTimeFormat" 29 | }, 30 | { 31 | path: "RelativeTimeFormat", 32 | name: "RelativeTimeFormat" 33 | }, 34 | { 35 | path: "ListFormat", 36 | name: "ListFormat" 37 | }, 38 | { 39 | path: "PluralRules", 40 | name: "PluralRules" 41 | }, 42 | { 43 | path: "Collator", 44 | name: "Collator" 45 | }, 46 | { 47 | path: "Segmenter", 48 | name: "Segmenter" 49 | }, 50 | { 51 | path: "DisplayNames", 52 | name: "DisplayNames" 53 | }, 54 | { 55 | path: "DurationFormat", 56 | name: "DurationFormat", 57 | experimental: true 58 | }, 59 | { 60 | path: "Locale", 61 | name: "Locale" 62 | } 63 | ]; 64 | -------------------------------------------------------------------------------- /src/lib/store/locales.ts: -------------------------------------------------------------------------------- 1 | import { browser } from "$app/environment"; 2 | import { getLocaleFromParams, localeKey } from "$utils/get-locale"; 3 | import { writable } from "svelte/store"; 4 | 5 | export const locales = writable(getLocaleFromParams()); 6 | 7 | locales.subscribe((value) => { 8 | if (browser) { 9 | const params = new URLSearchParams(window.location.search); 10 | params.set(localeKey, value.join(",")); 11 | const formatted = value.length ? `?${params.toString()}` : ""; 12 | window.history.replaceState( 13 | {}, 14 | "", 15 | `${window.location.origin}${window.location.pathname}${formatted}` 16 | ); 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /src/lib/store/settings.ts: -------------------------------------------------------------------------------- 1 | import { browser } from "$app/environment"; 2 | import { writable } from "svelte/store"; 3 | 4 | export type DarkMode = "dark" | "light"; 5 | 6 | type HintKeys = 7 | | "codeThemeHint" 8 | | "themeHint" 9 | | "showBrowserSupportHint" 10 | | "accentColorHint" 11 | | "announceOutputToScreenreaderHint"; 12 | 13 | type Setting = { 14 | type: "radio" | "checkbox" | "color"; 15 | values: Value[]; 16 | hint?: HintKeys; 17 | }; 18 | 19 | type SettingsConfiguration = { 20 | codeTheme: Setting; 21 | theme: Setting; 22 | showBrowserSupport: Setting; 23 | announceOutputToScreenreader: Setting; 24 | accentColor: Setting; 25 | }; 26 | 27 | export const settingsConfiguration: SettingsConfiguration = { 28 | codeTheme: { 29 | type: "radio", 30 | values: ["dark", "light"], 31 | hint: "codeThemeHint" 32 | }, 33 | theme: { 34 | type: "radio", 35 | values: ["dark", "light"], 36 | hint: "themeHint" 37 | }, 38 | showBrowserSupport: { 39 | type: "checkbox", 40 | values: [true], 41 | hint: "showBrowserSupportHint" 42 | }, 43 | accentColor: { 44 | type: "color", 45 | values: ["275"], 46 | hint: "accentColorHint" 47 | }, 48 | announceOutputToScreenreader: { 49 | type: "checkbox", 50 | values: [true], 51 | hint: "announceOutputToScreenreaderHint" 52 | } 53 | }; 54 | 55 | export type Settings = { 56 | codeTheme: DarkMode; 57 | theme: DarkMode; 58 | showBrowserSupport: boolean; 59 | accentColor: string; 60 | announceOutputToScreenreader: boolean; 61 | }; 62 | 63 | const defaultSettings: Settings = { 64 | codeTheme: "dark", 65 | theme: "light", 66 | showBrowserSupport: true, 67 | accentColor: "275", 68 | announceOutputToScreenreader: true 69 | }; 70 | 71 | export const settingsKeys = Object.keys(defaultSettings) as (keyof Settings)[]; 72 | 73 | export const settingsLocalStorageKey = "intl-explorer-settings"; 74 | 75 | const getSettings = () => { 76 | if (!browser) return defaultSettings; 77 | const stored = localStorage.getItem(settingsLocalStorageKey); 78 | if (!stored) return defaultSettings; 79 | return { 80 | ...defaultSettings, 81 | ...JSON.parse(stored) 82 | } as Settings; 83 | }; 84 | 85 | export const settings = writable(getSettings()); 86 | 87 | settings.subscribe((value) => { 88 | if (browser) { 89 | const htmlElement = document.querySelector("html"); 90 | htmlElement?.setAttribute(`data-${value.theme}-mode`, "true"); 91 | htmlElement?.removeAttribute(`data-${value.theme === "dark" ? "light" : "dark"}-mode`); 92 | if (value.codeTheme === "light") { 93 | document.querySelector("html")?.setAttribute("data-code-light-mode", "true"); 94 | } else if (value.codeTheme === "dark") { 95 | document.querySelector("html")?.removeAttribute("data-code-light-mode"); 96 | } 97 | if (value.accentColor) { 98 | document.documentElement.style.setProperty("--accent-hue", value.accentColor); 99 | } 100 | localStorage.setItem(settingsLocalStorageKey, JSON.stringify(value)); 101 | } 102 | }); 103 | -------------------------------------------------------------------------------- /src/lib/types/BrowserSupport.types.ts: -------------------------------------------------------------------------------- 1 | import type { BrowserType, VersionValue } from "@mdn/browser-compat-data"; 2 | 3 | export type BrowserCoverage = "full" | "partial" | "none"; 4 | 5 | export type BrowserReleaseData = { 6 | versionAdded: VersionValue; 7 | browserType: BrowserType; 8 | browserName: string; 9 | }; 10 | 11 | export type BrowserSupportForOption = { 12 | coverage?: BrowserCoverage; 13 | support?: Record; 14 | }; 15 | 16 | export type BrowserSupportDataForOptions = Record; 17 | 18 | export type BrowserSupportDataForMethod = { 19 | specUrl?: string | string[]; 20 | mdnUrl?: string; 21 | support?: Record; 22 | coverage?: BrowserCoverage; 23 | optionsSupport?: BrowserSupportDataForOptions; 24 | formattersSupport?: BrowserSupportDataForOptions; 25 | propertiesSupport?: BrowserSupportDataForOptions; 26 | }; 27 | -------------------------------------------------------------------------------- /src/lib/types/OptionValues.types.ts: -------------------------------------------------------------------------------- 1 | export type OptionValues = { [key: string]: number | boolean | string }; 2 | -------------------------------------------------------------------------------- /src/lib/types/common.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | /** 3 | * Makes every property of an object optional, even nested properties. 4 | */ 5 | type DeepPartial = { 6 | [P in keyof T]?: T[P] extends (infer U)[] 7 | ? DeepPartial[] 8 | : T[P] extends Readonly[] 9 | ? Readonly>[] 10 | : DeepPartial; 11 | }; 12 | 13 | /** 14 | * Ensures that the consumer can override any prop of the model 15 | * and that the return type of the factory matches the type 16 | * of the stub. 17 | * @example 18 | * ``` 19 | * type Person = { firstName: string, lastName: string }; 20 | * export const person: Factory = (overrides = {}) => ({ 21 | * firstName: "A default first name", 22 | * lastName: "A default last name", 23 | * // Any overrides from consumer 24 | * ...overrides 25 | * }) 26 | * ``` 27 | */ 28 | export type Factory = (props?: DeepPartial) => Model; 29 | 30 | // https://stackoverflow.com/questions/55127004/how-to-transform-union-type-to-tuple-type 31 | type UnionToIntersection = (U extends any ? (k: U) => void : never) extends (k: infer I) => void 32 | ? I 33 | : never; 34 | 35 | type LastOf = 36 | UnionToIntersection T : never> extends () => infer R ? R : never; 37 | 38 | type Push = [...T, V]; 39 | 40 | type TuplifyUnion, N = [T] extends [never] ? true : false> = true extends N 41 | ? [] 42 | : Push>, L>; 43 | 44 | export type Tuple = TuplifyUnion["length"] extends A["length"] 45 | ? [...A] 46 | : Tuple; 47 | 48 | export type CreateOptions = { 49 | [key in Key]: Tuple; 50 | }; 51 | 52 | export type StringOrUndefinedTuple = [string | undefined, string | undefined]; 53 | export type NumberOrUndefinedTuple = [number | undefined, number | undefined]; 54 | -------------------------------------------------------------------------------- /src/lib/utils/analytics.ts: -------------------------------------------------------------------------------- 1 | export type AnalyticEvents = "Copy Schema" | "Copy Code"; 2 | 3 | export const trackEvent = (name: AnalyticEvents, data?: Record) => { 4 | try { 5 | window?.umami?.track(name, data); 6 | } catch (error) { 7 | // noop 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /src/lib/utils/browser-support.ts: -------------------------------------------------------------------------------- 1 | import type { BrowserSupportDataForMethod } from "$types/BrowserSupport.types"; 2 | 3 | export const getSpecUrls = (data: BrowserSupportDataForMethod | null) => 4 | data && Array.isArray(data.specUrl) 5 | ? data.specUrl 6 | : typeof data?.specUrl === "string" 7 | ? [data.specUrl] 8 | : []; 9 | -------------------------------------------------------------------------------- /src/lib/utils/copy-to-clipboard.ts: -------------------------------------------------------------------------------- 1 | import { trackEvent } from "$utils/analytics"; 2 | 3 | export function copyToClipboard(textToCopy: string): Promise { 4 | if (navigator.clipboard && window.isSecureContext) { 5 | return navigator.clipboard.writeText(textToCopy); 6 | } else { 7 | const textArea = document.createElement("textarea"); 8 | textArea.value = textToCopy; 9 | textArea.style.position = "fixed"; 10 | textArea.style.left = "-999999px"; 11 | textArea.style.top = "-999999px"; 12 | document.body.appendChild(textArea); 13 | textArea.focus(); 14 | textArea.select(); 15 | return new Promise((resolve, reject) => { 16 | document.execCommand("copy") ? resolve() : reject(); 17 | textArea.remove(); 18 | }); 19 | } 20 | } 21 | 22 | export const copyCode = async (code: string) => { 23 | await copyToClipboard(code); 24 | trackEvent("Copy Code", { 25 | code 26 | }); 27 | }; 28 | -------------------------------------------------------------------------------- /src/lib/utils/directives.ts: -------------------------------------------------------------------------------- 1 | export function onClickOutside(element: HTMLElement, callback: () => void) { 2 | const onClick = (event: MouseEvent) => { 3 | const target = event.target as Node | null; 4 | if (!element.contains(target)) { 5 | callback(); 6 | } 7 | }; 8 | 9 | document.body.addEventListener("mousedown", onClick); 10 | 11 | return { 12 | update(newCallback: () => void) { 13 | callback = newCallback; 14 | }, 15 | 16 | destroy() { 17 | document.body.removeEventListener("mousedown", onClick); 18 | } 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /src/lib/utils/dom-utils.ts: -------------------------------------------------------------------------------- 1 | export const testIds = { 2 | navigation: "navigation", 3 | openNavigation: "openNavigation", 4 | optionSectionPrefix: "optionSection_", 5 | playground: { 6 | output: "playgroundOutput", 7 | code: "playgroundCode", 8 | resolvedOptions: "resolvedOptions" 9 | } 10 | } as const; 11 | -------------------------------------------------------------------------------- /src/lib/utils/factory.ts: -------------------------------------------------------------------------------- 1 | import type { PlaygroundOption, PlaygroundSchema } from "$lib/playground/playground.schema"; 2 | import type { Factory } from "$types/common"; 3 | 4 | export const numberFormatOptionFactory: Factory> = ( 5 | overrides = {} 6 | ) => ({ 7 | name: "minimumIntegerDigits", 8 | valueType: "number", 9 | inputType: "text", 10 | defaultValue: undefined, 11 | value: undefined, 12 | selected: undefined, 13 | ...overrides 14 | }); 15 | 16 | export const dateTimeFormatOptionFactory: Factory> = ( 17 | overrides = {} 18 | ) => ({ 19 | name: "hour", 20 | valueType: "number", 21 | inputType: "text", 22 | defaultValue: undefined, 23 | value: undefined, 24 | selected: undefined, 25 | ...overrides 26 | }); 27 | 28 | export const numberFormatSchemaFactory: Factory> = ( 29 | overrides = {} 30 | ) => ({ 31 | inputValues: [1091, 2000], 32 | ...overrides, 33 | method: "NumberFormat", 34 | primaryFormatter: "format", 35 | inputValueType: "number", 36 | invalidOptionCombos: {}, 37 | options: overrides?.options?.map(numberFormatOptionFactory) ?? [], 38 | secondaryFormatters: ["formatToParts", "formatRange", "formatRangeToParts"] 39 | }); 40 | 41 | export const htmlInputElementFactory: Factory = (overrides = {}) => { 42 | return { 43 | ...overrides 44 | } as HTMLInputElement; 45 | }; 46 | 47 | export const eventFactory: Factory & { target: HTMLInputElement | null }> = ( 48 | overrides = {} 49 | ) => { 50 | return { 51 | ...overrides, 52 | target: overrides.target ? htmlInputElementFactory(overrides.target) : null 53 | } as Omit & { target: HTMLInputElement | null }; 54 | }; 55 | -------------------------------------------------------------------------------- /src/lib/utils/format-utils.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from "vitest"; 2 | import { 3 | camelCaseToWords, 4 | clampValue, 5 | formatLocaleForUrl, 6 | formatLocalesForPrint, 7 | tryFormat 8 | } from "./format-utils"; 9 | import { numberFormatOptionFactory } from "./factory"; 10 | 11 | describe("clampValue", () => { 12 | test("clamp max", () => { 13 | expect( 14 | clampValue( 15 | numberFormatOptionFactory({ 16 | valueType: "number", 17 | max: 9 18 | }), 19 | "12" 20 | ) 21 | ).toEqual(9); 22 | }); 23 | 24 | test("clamp min", () => { 25 | expect( 26 | clampValue( 27 | numberFormatOptionFactory({ 28 | valueType: "number", 29 | min: 1 30 | }), 31 | "-19" 32 | ) 33 | ).toEqual(1); 34 | }); 35 | 36 | test("no clamp if no number", () => { 37 | expect( 38 | clampValue( 39 | numberFormatOptionFactory({ 40 | valueType: "boolean", 41 | min: 1 42 | }), 43 | "true" 44 | ) 45 | ).toEqual("true"); 46 | }); 47 | 48 | test("fallback to defaultValue if faulty value", () => { 49 | expect( 50 | clampValue( 51 | numberFormatOptionFactory({ 52 | valueType: "number", 53 | min: 1 54 | }), 55 | "true" 56 | ) 57 | ).toEqual(undefined); 58 | }); 59 | }); 60 | 61 | describe("tryFormat", () => { 62 | test("fail on faulty format", () => { 63 | expect( 64 | tryFormat(() => 65 | Intl.NumberFormat(undefined, { 66 | style: "currency" 67 | }).format(19) 68 | ) 69 | ).toEqual("Currency code is required with currency style."); 70 | }); 71 | 72 | test("output of fine format", () => { 73 | expect( 74 | tryFormat(() => 75 | Intl.NumberFormat(undefined, { 76 | style: "currency", 77 | currency: "USD" 78 | }).format(19) 79 | ) 80 | ).toEqual("$19.00"); 81 | }); 82 | }); 83 | 84 | describe("camelCaseToWords", () => { 85 | test("convert", () => { 86 | expect(camelCaseToWords("iAmACamel")).toEqual("I Am A Camel"); 87 | }); 88 | }); 89 | 90 | describe("formatLocalesForPrint", () => { 91 | test("format one locale", () => { 92 | expect(formatLocalesForPrint(["sv"])).toEqual('"sv"'); 93 | }); 94 | 95 | test("format multiple locale", () => { 96 | expect(formatLocalesForPrint(["sv", "en"])).toEqual('["sv","en"]'); 97 | }); 98 | 99 | test("format zero locale", () => { 100 | expect(formatLocalesForPrint([])).toEqual("undefined"); 101 | }); 102 | }); 103 | 104 | describe("formatLocaleForUrl", () => { 105 | test("format locales", () => { 106 | expect(formatLocaleForUrl(["sv"])).toEqual("?locale=sv"); 107 | }); 108 | test("format locales", () => { 109 | expect(formatLocaleForUrl(["sv", "en"])).toEqual("?locale=sv,en"); 110 | }); 111 | }); 112 | -------------------------------------------------------------------------------- /src/lib/utils/format-utils.ts: -------------------------------------------------------------------------------- 1 | import type { FormatMethodsKeys } from "$lib/format-methods"; 2 | import type { PlaygroundOption } from "$lib/playground/playground.schema"; 3 | 4 | export const defaultNumberRange = [1, 2, 3, 4, 5]; 5 | 6 | export const clampValue = ( 7 | option: PlaygroundOption, 8 | value: string | boolean | null | undefined 9 | ) => { 10 | if (option.valueType !== "number") return value; 11 | if (typeof value !== "string") return value; 12 | const parsed = parseInt(value, 10); 13 | if (isNaN(parsed)) return option.defaultValue; 14 | if (!option.max && !option.min) return parsed; 15 | const clampedMax = option.max && parsed > option.max ? option.max : parsed; 16 | return option.min && parsed < option.min ? Math.max(option.min, clampedMax) : clampedMax; 17 | }; 18 | 19 | export const print = (values: unknown) => { 20 | return JSON.stringify(values, null, 2); 21 | }; 22 | 23 | export const fallbackDisplayNames: Record = { 24 | language: "en-US", 25 | region: "US", 26 | script: "Latn", 27 | calendar: "gregory", 28 | dateTimeField: "month", 29 | currency: "USD" 30 | }; 31 | 32 | export const tryDisplayNames = ( 33 | code: string, 34 | language: string[], 35 | options: Intl.DisplayNamesOptions 36 | ): string => { 37 | try { 38 | return new Intl.DisplayNames(language, options).of(code) ?? fallbackDisplayNames[options.type]; 39 | } catch (e) { 40 | return fallbackDisplayNames[options.type]; 41 | } 42 | }; 43 | 44 | export const tryFormat = (format: () => string): string => { 45 | try { 46 | return format(); 47 | } catch (error) { 48 | return `${(error as { message: string }).message}`; 49 | } 50 | }; 51 | 52 | export const camelCaseToWords = (s: string) => { 53 | const result = s.replace(/([A-Z])/g, " $1"); 54 | return result.charAt(0).toUpperCase() + result.slice(1); 55 | }; 56 | 57 | export const formatLocalesForPrint = (locales: string[]) => { 58 | if (!locales.length) { 59 | return `undefined`; 60 | } 61 | if (locales.length === 1) { 62 | return `"${locales[0]}"`; 63 | } 64 | return `[${locales.map((l) => `"${l}"`).join(",")}]`; 65 | }; 66 | 67 | export const formatLocaleForUrl = (locales: string[]) => { 68 | if (locales.length) { 69 | return `?locale=${locales.join(",")}`; 70 | } 71 | return ""; 72 | }; 73 | -------------------------------------------------------------------------------- /src/lib/utils/get-locale.ts: -------------------------------------------------------------------------------- 1 | import { browser } from "$app/environment"; 2 | 3 | export const localeKey = "locale"; 4 | 5 | export const localeFallback = browser ? window.navigator.language : "en-US"; 6 | 7 | export const getLocaleFromParams = (): string[] => { 8 | if (!browser) { 9 | return []; 10 | } 11 | return new URLSearchParams(window.location.search).get(localeKey)?.split(",") ?? []; 12 | }; 13 | -------------------------------------------------------------------------------- /src/lib/utils/load-json.ts: -------------------------------------------------------------------------------- 1 | import type { FormatMethodsKeys } from "$lib/format-methods"; 2 | 3 | export const loadJson = async ( 4 | method: FormatMethodsKeys | "Locale" | "Playground" 5 | ): Promise => { 6 | try { 7 | const response = await fetch(`/${method}-compat-data.json`); 8 | return response.json(); 9 | } catch (e: unknown) { 10 | return undefined; 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | Intl Explorer 12 | 13 | 14 | {#if navigating} 15 | 16 | {/if} 17 | 18 |
19 |

{m.blurbWelcome()} 👋🏽

20 |
21 | 22 |
23 |

24 | Intl Explorer 25 | {m.blurbPart1()} 26 | ECMAScript Internationalization API. {m.blurbPart2()} Intl 31 | {m.blurbPart3()} 32 |

33 | 34 |

35 | {m.blurbPart4()} The Playground 36 | {m.blurbPart5()} 37 |

38 | 39 |

40 | {m.blurbPart6()} 41 | GitHub. {m.blurbPart7()} 44 | Jesper Orb. 45 |

46 |
47 | 48 | 53 | -------------------------------------------------------------------------------- /src/routes/Collator/+page.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | {#await browserCompatData} 17 | 18 | 19 | 20 | {:then data} 21 | 22 | 23 | 24 | {/await} 25 | -------------------------------------------------------------------------------- /src/routes/DateTimeFormat/+page.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | {#await browserCompatData} 17 | 18 | 19 | 20 | {:then data} 21 | 22 | 23 | 24 | {/await} 25 | -------------------------------------------------------------------------------- /src/routes/DisplayNames/+page.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | {#await browserCompatData} 17 | 18 | 19 | 20 | {:then data} 21 | 22 | 23 | 24 | {/await} 25 | -------------------------------------------------------------------------------- /src/routes/DurationFormat/+page.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | {#await browserCompatData} 17 | 18 | 19 | 20 | {:then data} 21 | 22 | 23 | 24 | {/await} 25 | -------------------------------------------------------------------------------- /src/routes/ListFormat/+page.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | {#await browserCompatData} 17 | 18 | 19 | 20 | {:then data} 21 | 22 | 23 | 24 | {/await} 25 | -------------------------------------------------------------------------------- /src/routes/Locale/+page.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | {#await browserCompatData} 17 | 18 | 19 | 20 | {:then data} 21 | 22 | 23 | 24 | {/await} 25 | -------------------------------------------------------------------------------- /src/routes/NumberFormat/+page.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | {#await browserCompatData} 18 | 19 | 20 |

21 | {m.seeAlso()} Currency 22 | {m.and()} Unit. 23 |

24 | 25 | 26 | {:then data} 27 | 28 | 29 |

30 | {m.seeAlso()} Currency 31 | {m.and()} Unit. 32 |

33 | 34 | 35 | {/await} 36 | -------------------------------------------------------------------------------- /src/routes/NumberFormat/Currency/+page.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | {#await browserCompatData} 17 | 18 | 19 | 20 | {:then data} 21 | 22 | 23 | 24 | {/await} 25 | -------------------------------------------------------------------------------- /src/routes/NumberFormat/Unit/+page.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | {#await browserCompatData} 17 | 18 | {:then data} 19 | {#if $settings.showBrowserSupport} 20 | 21 | 22 | {/if} 23 | 24 | {/await} 25 | -------------------------------------------------------------------------------- /src/routes/Playground/+page.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | {#if $settings.codeTheme === "light"} 19 | {@html github} 20 | {:else} 21 | {@html oceanicNext} 22 | {/if} 23 | 24 | 25 | {#await browserSupport} 26 | 27 | {:then data} 28 | {#if data} 29 | 30 | {/if} 31 | {/await} 32 | -------------------------------------------------------------------------------- /src/routes/PluralRules/+page.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | {#await browserCompatData} 17 | 18 | 19 | 20 | {:then data} 21 | 22 | 23 | 24 | {/await} 25 | -------------------------------------------------------------------------------- /src/routes/RelativeTimeFormat/+page.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | {#await browserCompatData} 17 | 18 | 19 | 20 | {:then data} 21 | 22 | 23 | 24 | {/await} 25 | -------------------------------------------------------------------------------- /src/routes/Segmenter/+page.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | {#await browserCompatData} 16 | 17 | 18 | 19 | {:then data} 20 | 21 | 22 | 23 | {/await} 24 | -------------------------------------------------------------------------------- /src/routes/routing-links.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 |
7 | {#each locales as locale} 8 | {locale} 9 | {/each} 10 |
11 | -------------------------------------------------------------------------------- /src/update-compat-data.ts: -------------------------------------------------------------------------------- 1 | import { writeCompatData } from "$utils/write-compat-data"; 2 | 3 | writeCompatData(); 4 | -------------------------------------------------------------------------------- /static/DisplayNames-compat-data.json: -------------------------------------------------------------------------------- 1 | {"mdnUrl":"https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Intl/DisplayNames","specUrl":"https://tc39.es/ecma402/#intl-displaynames-objects","support":{"chrome":{"browserName":"Chrome","versionAdded":"81","browserType":"desktop"},"edge":{"browserName":"Edge","versionAdded":"81","browserType":"desktop"},"firefox":{"browserName":"Firefox","versionAdded":"86","browserType":"desktop"},"opera":{"browserName":"Opera","versionAdded":"68","browserType":"desktop"},"safari":{"browserName":"Safari","versionAdded":"14.1","browserType":"desktop"},"chrome_android":{"browserName":"Chrome Android","versionAdded":"81","browserType":"mobile"},"firefox_android":{"browserName":"Firefox for Android","versionAdded":"86","browserType":"mobile"},"opera_android":{"browserName":"Opera Android","versionAdded":"58","browserType":"mobile"},"safari_ios":{"browserName":"Safari on iOS","versionAdded":"14.5","browserType":"mobile"},"samsunginternet_android":{"browserName":"Samsung Internet","versionAdded":"13.0","browserType":"mobile"},"webview_android":{"browserName":"WebView Android","versionAdded":"81","browserType":"mobile"},"webview_ios":{"browserName":"WebView on iOS","versionAdded":"14.5","browserType":"mobile"},"deno":{"browserName":"Deno","versionAdded":"1.8","browserType":"server"},"nodejs":{"browserName":"Node.js","versionAdded":"14.0.0","browserType":"server"}},"coverage":"full","optionsSupport":{},"formattersSupport":{}} -------------------------------------------------------------------------------- /static/DurationFormat-compat-data.json: -------------------------------------------------------------------------------- 1 | {"mdnUrl":"https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat","specUrl":"https://tc39.es/proposal-intl-duration-format/#durationformat-objects","support":{"chrome":{"browserName":"Chrome","versionAdded":"129","browserType":"desktop"},"edge":{"browserName":"Edge","versionAdded":"129","browserType":"desktop"},"firefox":{"browserName":"Firefox","versionAdded":"136","browserType":"desktop"},"opera":{"browserName":"Opera","versionAdded":"115","browserType":"desktop"},"safari":{"browserName":"Safari","versionAdded":"16.4","browserType":"desktop"},"chrome_android":{"browserName":"Chrome Android","versionAdded":"129","browserType":"mobile"},"firefox_android":{"browserName":"Firefox for Android","versionAdded":"136","browserType":"mobile"},"opera_android":{"browserName":"Opera Android","versionAdded":"86","browserType":"mobile"},"safari_ios":{"browserName":"Safari on iOS","versionAdded":"16.4","browserType":"mobile"},"samsunginternet_android":{"browserName":"Samsung Internet","versionAdded":false,"browserType":"mobile"},"webview_android":{"browserName":"WebView Android","versionAdded":"129","browserType":"mobile"},"webview_ios":{"browserName":"WebView on iOS","versionAdded":"16.4","browserType":"mobile"},"deno":{"browserName":"Deno","versionAdded":"1.46","browserType":"server"},"nodejs":{"browserName":"Node.js","versionAdded":"23.0.0","browserType":"server"}},"coverage":"partial","optionsSupport":{},"formattersSupport":{"formatToParts":{"coverage":"partial","support":{"chrome":{"browserName":"Chrome","versionAdded":"129","browserType":"desktop"},"edge":{"browserName":"Edge","versionAdded":"129","browserType":"desktop"},"firefox":{"browserName":"Firefox","versionAdded":"136","browserType":"desktop"},"opera":{"browserName":"Opera","versionAdded":"115","browserType":"desktop"},"safari":{"browserName":"Safari","versionAdded":"16.4","browserType":"desktop"},"chrome_android":{"browserName":"Chrome Android","versionAdded":"129","browserType":"mobile"},"firefox_android":{"browserName":"Firefox for Android","versionAdded":"136","browserType":"mobile"},"opera_android":{"browserName":"Opera Android","versionAdded":"86","browserType":"mobile"},"safari_ios":{"browserName":"Safari on iOS","versionAdded":"16.4","browserType":"mobile"},"samsunginternet_android":{"browserName":"Samsung Internet","versionAdded":false,"browserType":"mobile"},"webview_android":{"browserName":"WebView Android","versionAdded":"129","browserType":"mobile"},"webview_ios":{"browserName":"WebView on iOS","versionAdded":"16.4","browserType":"mobile"},"deno":{"browserName":"Deno","versionAdded":"1.46","browserType":"server"},"nodejs":{"browserName":"Node.js","versionAdded":"23.0.0","browserType":"server"}}}}} -------------------------------------------------------------------------------- /static/ListFormat-compat-data.json: -------------------------------------------------------------------------------- 1 | {"mdnUrl":"https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Intl/ListFormat","specUrl":"https://tc39.es/ecma402/#listformat-objects","support":{"chrome":{"browserName":"Chrome","versionAdded":"72","browserType":"desktop"},"edge":{"browserName":"Edge","versionAdded":"79","browserType":"desktop"},"firefox":{"browserName":"Firefox","versionAdded":"78","browserType":"desktop"},"opera":{"browserName":"Opera","versionAdded":"60","browserType":"desktop"},"safari":{"browserName":"Safari","versionAdded":"14.1","browserType":"desktop"},"chrome_android":{"browserName":"Chrome Android","versionAdded":"72","browserType":"mobile"},"firefox_android":{"browserName":"Firefox for Android","versionAdded":"79","browserType":"mobile"},"opera_android":{"browserName":"Opera Android","versionAdded":"51","browserType":"mobile"},"safari_ios":{"browserName":"Safari on iOS","versionAdded":"14.5","browserType":"mobile"},"samsunginternet_android":{"browserName":"Samsung Internet","versionAdded":"11.0","browserType":"mobile"},"webview_android":{"browserName":"WebView Android","versionAdded":"72","browserType":"mobile"},"webview_ios":{"browserName":"WebView on iOS","versionAdded":"14.5","browserType":"mobile"},"deno":{"browserName":"Deno","versionAdded":"1.8","browserType":"server"},"nodejs":{"browserName":"Node.js","versionAdded":"12.0.0","browserType":"server"}},"coverage":"full","optionsSupport":{},"formattersSupport":{"formatToParts":{"coverage":"full","support":{"chrome":{"browserName":"Chrome","versionAdded":"72","browserType":"desktop"},"edge":{"browserName":"Edge","versionAdded":"79","browserType":"desktop"},"firefox":{"browserName":"Firefox","versionAdded":"78","browserType":"desktop"},"opera":{"browserName":"Opera","versionAdded":"60","browserType":"desktop"},"safari":{"browserName":"Safari","versionAdded":"14.1","browserType":"desktop"},"chrome_android":{"browserName":"Chrome Android","versionAdded":"72","browserType":"mobile"},"firefox_android":{"browserName":"Firefox for Android","versionAdded":"79","browserType":"mobile"},"opera_android":{"browserName":"Opera Android","versionAdded":"51","browserType":"mobile"},"safari_ios":{"browserName":"Safari on iOS","versionAdded":"14.5","browserType":"mobile"},"samsunginternet_android":{"browserName":"Samsung Internet","versionAdded":"11.0","browserType":"mobile"},"webview_android":{"browserName":"WebView Android","versionAdded":"72","browserType":"mobile"},"webview_ios":{"browserName":"WebView on iOS","versionAdded":"14.5","browserType":"mobile"},"deno":{"browserName":"Deno","versionAdded":"1.8","browserType":"server"},"nodejs":{"browserName":"Node.js","versionAdded":"12.0.0","browserType":"server"}}}}} -------------------------------------------------------------------------------- /static/Segmenter-compat-data.json: -------------------------------------------------------------------------------- 1 | {"mdnUrl":"https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Intl/Segmenter","specUrl":"https://tc39.es/ecma402/#segmenter-objects","support":{"chrome":{"browserName":"Chrome","versionAdded":"87","browserType":"desktop"},"edge":{"browserName":"Edge","versionAdded":"87","browserType":"desktop"},"firefox":{"browserName":"Firefox","versionAdded":"125","browserType":"desktop"},"opera":{"browserName":"Opera","versionAdded":"73","browserType":"desktop"},"safari":{"browserName":"Safari","versionAdded":"14.1","browserType":"desktop"},"chrome_android":{"browserName":"Chrome Android","versionAdded":"87","browserType":"mobile"},"firefox_android":{"browserName":"Firefox for Android","versionAdded":"125","browserType":"mobile"},"opera_android":{"browserName":"Opera Android","versionAdded":"62","browserType":"mobile"},"safari_ios":{"browserName":"Safari on iOS","versionAdded":"14.5","browserType":"mobile"},"samsunginternet_android":{"browserName":"Samsung Internet","versionAdded":"14.0","browserType":"mobile"},"webview_android":{"browserName":"WebView Android","versionAdded":"87","browserType":"mobile"},"webview_ios":{"browserName":"WebView on iOS","versionAdded":"14.5","browserType":"mobile"},"deno":{"browserName":"Deno","versionAdded":"1.8","browserType":"server"},"nodejs":{"browserName":"Node.js","versionAdded":"16.0.0","browserType":"server"}},"coverage":"full","optionsSupport":{},"formattersSupport":{}} -------------------------------------------------------------------------------- /static/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /static/icons/chrome_partial_support.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /static/icons/chrome_supported.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /static/icons/chrome_unsupported.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /static/icons/desktop.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 14 | 15 | 19 | 20 | -------------------------------------------------------------------------------- /static/icons/edge_partial_support.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /static/icons/edge_supported.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /static/icons/edge_unsupported.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /static/icons/experimental.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/icons/firefox_partial_support.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /static/icons/firefox_supported.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /static/icons/firefox_unsupported.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /static/icons/mobile.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /static/icons/nodejs_partial_support.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /static/icons/nodejs_supported.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /static/icons/nodejs_unsupported.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /static/icons/opera_partial_support.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /static/icons/opera_supported.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /static/icons/opera_unsupported.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /static/icons/safari_partial_support.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /static/icons/safari_supported.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /static/icons/safari_unsupported.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /static/icons/samsunginternet_partial_support.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /static/icons/samsunginternet_supported.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /static/icons/samsunginternet_unsupported.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /static/icons/server.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /static/icons/webview_partial_support.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /static/icons/webview_supported.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /static/icons/webview_unsupported.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /static/social.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jesperorb/intl-explorer/7a8afde8e90ded04ed09c6ad972ee46589c5c42f/static/social.png -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from "@sveltejs/adapter-cloudflare"; 2 | import { sveltePreprocess } from "svelte-preprocess"; 3 | import { preprocessMeltUI, sequence } from "@melt-ui/pp"; 4 | 5 | /** @type {import('@sveltejs/kit').Config} */ 6 | const config = { 7 | // Consult https://github.com/sveltejs/svelte-preprocess 8 | // for more information about preprocessors 9 | preprocess: sequence([sveltePreprocess(), preprocessMeltUI()]), 10 | 11 | kit: { 12 | adapter: adapter(), 13 | alias: { 14 | $paraglide: "./src/paraglide", 15 | $ui: "./src/lib/components/ui", 16 | $pages: "./src/lib/components/pages", 17 | $utils: "./src/lib/utils", 18 | $i18n: "./src/lib/i18n", 19 | $store: "./src/lib/store", 20 | $types: "./src/lib/types" 21 | } 22 | } 23 | }; 24 | 25 | export default config; 26 | -------------------------------------------------------------------------------- /tests/constants.ts: -------------------------------------------------------------------------------- 1 | export const mdnUrl = 2 | "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl"; 3 | export const localePort = "5173"; 4 | export const localBaseURL = `http://localhost:${localePort}`; 5 | export const defaultPageUnderTest = "/"; 6 | -------------------------------------------------------------------------------- /tests/methods.spec.ts: -------------------------------------------------------------------------------- 1 | import { formatMethods } from "../src/lib/format-methods"; 2 | import { test } from "./test"; 3 | 4 | test("visits and sees all method pages", async ({ intlPage }) => { 5 | await test.step("goes to page", async () => { 6 | await intlPage.goesToStart(); 7 | }); 8 | for (const method of formatMethods) { 9 | await test.step("goes to page", async () => { 10 | intlPage.setPageUnderTest(method); 11 | await intlPage.goesToPage(); 12 | }); 13 | 14 | await test.step("verifies default values", async () => { 15 | await intlPage.assertMDNLink(); 16 | await intlPage.assertTitle(); 17 | }); 18 | 19 | await test.step("changes input value and checks result", async () => { 20 | await intlPage.changesValueAndSeesExpectedOutput(method); 21 | }); 22 | } 23 | 24 | await test.step("changes locale and sees locale change", async () => { 25 | const locale = "sv-SE"; 26 | await intlPage.changesLocale(locale); 27 | await intlPage.assertUrlLocale(locale); 28 | await intlPage.seesSelectedLocaleChip(locale); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /tests/settings.spec.ts: -------------------------------------------------------------------------------- 1 | import { test } from "./test"; 2 | 3 | test("user changes settings", async ({ settings, playground }) => { 4 | await test.step("user navigates to settings page", async () => { 5 | await settings.goesToStart(); 6 | await settings.open(); 7 | }); 8 | 9 | await test.step("user selects dark theme mode", async () => { 10 | await settings.seesThemeMode("light"); 11 | await settings.selectsThemeMode("dark"); 12 | await settings.seesThemeMode("dark"); 13 | }); 14 | 15 | await test.step("user selects dark code theme mode", async () => { 16 | await settings.seesCodeThemeMode("dark"); 17 | await settings.selectsCodeThemeMode("light"); 18 | await settings.seesCodeThemeMode("light"); 19 | }); 20 | 21 | await test.step("user hides browser support", async () => { 22 | const formatter: keyof Intl.NumberFormat = "formatToParts"; 23 | await settings.close(); 24 | await playground.goesToPage(); 25 | await playground.seesBrowserSupport(true); 26 | await playground.seesBrowserSupportForSecondaryFormatter(formatter, true); 27 | await settings.open(); 28 | await settings.togglesBrowserSupport(false); 29 | await settings.close(); 30 | await playground.seesBrowserSupport(false); 31 | await playground.seesBrowserSupportForSecondaryFormatter(formatter, false); 32 | }); 33 | 34 | await test.step("user changes language", async () => { 35 | await settings.goesToStart(); 36 | await settings.open(); 37 | await settings.selectsLanguage("sv"); 38 | await settings.assertUrlLanguage("sv"); 39 | await settings.assertHtmlLanguage("sv"); 40 | settings.changeDictionaryLanguage("sv"); 41 | await settings.open(); 42 | await settings.seesHeadingForLanguage("sv"); 43 | }); 44 | 45 | await test.step.skip("user changes accent color", async () => { 46 | await settings.seesDefaultAccentColor(); 47 | await settings.changesAccentColor(-100); 48 | await settings.doesNotSeeDefaultAccentColor(); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /tests/test.ts: -------------------------------------------------------------------------------- 1 | import { test as base } from "@playwright/test"; 2 | import { IntlPage } from "./pages/IntlPage"; 3 | import { PlaygroundPage } from "./pages/PlaygroundPage"; 4 | import { SettingsPage } from "./pages/SettingsPage"; 5 | 6 | export type Test = { 7 | intlPage: IntlPage; 8 | playground: PlaygroundPage; 9 | settings: SettingsPage; 10 | }; 11 | 12 | export const test = base.extend({ 13 | intlPage: async ({ page }, use) => { 14 | const intlPage = new IntlPage({ page }); 15 | await use(intlPage); 16 | }, 17 | playground: async ({ page }, use) => { 18 | const playgroundPage = new PlaygroundPage({ page }); 19 | playgroundPage.setPageUnderTest("Playground"); 20 | await use(playgroundPage); 21 | }, 22 | settings: async ({ page }, use) => { 23 | const settings = new SettingsPage({ page }); 24 | await use(settings); 25 | } 26 | }); 27 | 28 | export { expect } from "@playwright/test"; 29 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "moduleResolution": "bundler", 13 | "lib": ["ES2018.Intl", "es2021.intl", "ES2020.Intl", "ES2022.Intl", "DOM"] 14 | } 15 | // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias 16 | // 17 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 18 | // from the referenced tsconfig.json - TypeScript does not merge them in 19 | } 20 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { sveltekit } from "@sveltejs/kit/vite"; 2 | import type { UserConfig } from "vite"; 3 | import { defineConfig, mergeConfig } from "vitest/config"; 4 | import { paraglideVitePlugin } from "@inlang/paraglide-js"; 5 | 6 | const config: UserConfig = { 7 | plugins: [ 8 | sveltekit(), 9 | paraglideVitePlugin({ 10 | project: "./project.inlang", 11 | outdir: "./src/paraglide", 12 | strategy: ["url"], 13 | disableAsyncLocalStorage: true 14 | }) 15 | ], 16 | optimizeDeps: { 17 | include: ["highlight.js", "highlight.js/lib/core"] 18 | } 19 | }; 20 | 21 | export default mergeConfig( 22 | config, 23 | defineConfig({ 24 | test: { 25 | environment: "jsdom", 26 | include: ["src/**/*.{test,spec}.ts"] 27 | } 28 | }) 29 | ); 30 | --------------------------------------------------------------------------------