diff --git a/.env.development b/.env.development
index e69de29..7652eb4 100644
--- a/.env.development
+++ b/.env.development
@@ -0,0 +1 @@
+REACT_APP_TRANSMISSION_RPC=http://tr.zj9495.com:9091
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 9fb8148..2f78edb 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -1,8 +1,10 @@
-name: Deploy To GCP
+name: Deploy preview environment
on:
push:
branches:
- master
+ pull_request:
+ branches: [master]
jobs:
build-and-deploy:
name: Deploy
@@ -36,9 +38,9 @@ jobs:
- uses: appleboy/scp-action@master
with:
host: tr.zj9495.com
- username: zj9495
+ username: ubuntu
port: 22
- key: ${{ secrets.DEPLOY_SSH_KEY }}
+ key: ${{ secrets.DEPLOY_PREVIEW_SSH_KEY }}
source: "build/*"
- target: "/mnt/disks/disk1/transmission/transmission-client"
+ target: "/home/ubuntu/transmission-client"
strip_components: 1
diff --git a/.github/workflows/e2e-test.chrome.yml b/.github/workflows/e2e-test.chrome.yml
new file mode 100644
index 0000000..408e63a
--- /dev/null
+++ b/.github/workflows/e2e-test.chrome.yml
@@ -0,0 +1,59 @@
+name: E2E on Chrome
+
+on:
+ # Triggers the workflow on push or pull request events but only for the master branch
+ push:
+ branches: [master]
+ pull_request:
+ branches: [master]
+
+jobs:
+ e2e-on-chrome:
+ runs-on: ubuntu-20.04
+ strategy:
+ # when one test fails, DO NOT cancel the other
+ # containers, because this will kill Cypress processes
+ # leaving the Dashboard hanging ...
+ # https://github.com/cypress-io/github-action/issues/48
+ fail-fast: false
+ matrix:
+ # run 4 copies of the current job in parallel
+ containers: [1, 2, 3, 4]
+ services:
+ transmission-rpc:
+ image: linuxserver/transmission:latest
+ env:
+ PUID: 1000
+ PGID: 1000
+ TZ: Europe/London
+ USER: zj9495
+ PASS: zj9495
+ ports:
+ - 9091:9091
+ nginx:
+ image: nginx:latest
+ env:
+ NGINX_PORT: 80
+ ports:
+ - 8080:80
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v1
+ # Install NPM dependencies, cache them correctly
+ # and run all Cypress tests
+ - name: Cypress run
+ uses: cypress-io/github-action@v2
+ with:
+ browser: chrome
+ headless: true
+ start: npm start
+ wait-on: "http://localhost:8888"
+ record: true
+ parallel: true
+ env:
+ # pass the Dashboard record key as an environment variable
+ CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
+ # pass GitHub token to allow accurately detecting a build vs a re-run build
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ REACT_APP_TRANSMISSION_RPC: http://localhost:9091
+ REACT_APP_RPC_DELAY: 100
diff --git a/.github/workflows/e2e-test.edge.yml b/.github/workflows/e2e-test.edge.yml
new file mode 100644
index 0000000..1e5baf3
--- /dev/null
+++ b/.github/workflows/e2e-test.edge.yml
@@ -0,0 +1,62 @@
+name: E2E on Edge
+
+on:
+ # Triggers the workflow on push or pull request events but only for the master branch
+ push:
+ branches: [master]
+ pull_request:
+ branches: [master]
+
+jobs:
+ e2e-on-edge:
+ runs-on: ubuntu-20.04
+ container:
+ image: cypress/browsers:node14.10.1-edge88
+ options: --user 1001
+ strategy:
+ # when one test fails, DO NOT cancel the other
+ # containers, because this will kill Cypress processes
+ # leaving the Dashboard hanging ...
+ # https://github.com/cypress-io/github-action/issues/48
+ fail-fast: false
+ matrix:
+ # run 4 copies of the current job in parallel
+ containers: [1, 2, 3, 4]
+ services:
+ transmission-rpc:
+ image: linuxserver/transmission:latest
+ env:
+ PUID: 1000
+ PGID: 1000
+ TZ: Europe/London
+ USER: zj9495
+ PASS: zj9495
+ ports:
+ - 9091:9091
+ nginx:
+ image: nginx:latest
+ env:
+ NGINX_PORT: 80
+ ports:
+ - 8080:80
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v1
+ # Install NPM dependencies, cache them correctly
+ # and run all Cypress tests
+ - name: Cypress run
+ uses: cypress-io/github-action@v2
+ with:
+ browser: edge
+ headless: true
+ start: npm start
+ wait-on: "http://localhost:8888"
+ record: true
+ parallel: true
+ env:
+ # pass the Dashboard record key as an environment variable
+ CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
+ # pass GitHub token to allow accurately detecting a build vs a re-run build
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ REACT_APP_TRANSMISSION_RPC: http://transmission-rpc:9091
+ REACT_APP_RPC_DELAY: 100
diff --git a/.github/workflows/e2e-test.ff.yml b/.github/workflows/e2e-test.ff.yml
new file mode 100644
index 0000000..febf386
--- /dev/null
+++ b/.github/workflows/e2e-test.ff.yml
@@ -0,0 +1,61 @@
+name: E2E on FireFox
+
+on:
+ # Triggers the workflow on push or pull request events but only for the master branch
+ push:
+ branches: [master]
+ pull_request:
+ branches: [master]
+
+jobs:
+ e2e-on-firefox:
+ runs-on: ubuntu-20.04
+ container:
+ image: cypress/browsers:node14.16.0-chrome90-ff88
+ options: --user 1001
+ strategy:
+ # when one test fails, DO NOT cancel the other
+ # containers, because this will kill Cypress processes
+ # leaving the Dashboard hanging ...
+ # https://github.com/cypress-io/github-action/issues/48
+ fail-fast: false
+ matrix:
+ # run 4 copies of the current job in parallel
+ containers: [1, 2, 3, 4]
+ services:
+ transmission-rpc:
+ image: linuxserver/transmission:latest
+ env:
+ PUID: 1000
+ PGID: 1000
+ TZ: Europe/London
+ USER: zj9495
+ PASS: zj9495
+ ports:
+ - 9091:9091
+ nginx:
+ image: nginx:latest
+ env:
+ NGINX_PORT: 80
+ ports:
+ - 8080:80
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v1
+ # Install NPM dependencies, cache them correctly
+ # and run all Cypress tests
+ - name: Cypress run
+ uses: cypress-io/github-action@v2
+ with:
+ browser: firefox
+ start: npm start
+ wait-on: "http://localhost:8888"
+ record: true
+ parallel: true
+ env:
+ # pass the Dashboard record key as an environment variable
+ CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
+ # pass GitHub token to allow accurately detecting a build vs a re-run build
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ REACT_APP_TRANSMISSION_RPC: http://transmission-rpc:9091
+ REACT_APP_RPC_DELAY: 100
diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml
deleted file mode 100644
index cbd7d79..0000000
--- a/.github/workflows/e2e-test.yml
+++ /dev/null
@@ -1,28 +0,0 @@
-name: End-to-end tests
-
-on:
- # Triggers the workflow on push or pull request events but only for the master branch
- push:
- branches: [master]
- pull_request:
- branches: [master]
-
-jobs:
- cypress-run:
- runs-on: ubuntu-20.04
- steps:
- - name: Checkout
- uses: actions/checkout@v1
- # Install NPM dependencies, cache them correctly
- # and run all Cypress tests
- - name: Cypress run
- uses: cypress-io/github-action@v2
- with:
- start: npm start
- wait-on: "http://localhost:8888"
- record: true
- env:
- # pass the Dashboard record key as an environment variable
- CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
- # pass GitHub token to allow accurately detecting a build vs a re-run build
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/cypress.env.json b/cypress.env.json
new file mode 100644
index 0000000..173ce5b
--- /dev/null
+++ b/cypress.env.json
@@ -0,0 +1,8 @@
+{
+ "protocol": "http",
+ "host": "localhost",
+ "port": 8888,
+ "path": "/transmission/web",
+ "user": "zj9495",
+ "password": "zj9495"
+}
\ No newline at end of file
diff --git a/cypress/integration/add.spec.js b/cypress/integration/add.spec.js
index 6a262b0..b2e48e8 100644
--- a/cypress/integration/add.spec.js
+++ b/cypress/integration/add.spec.js
@@ -1,6 +1,6 @@
///
-import { removeTestTorrent } from "./common"
+import { removeTestTorrent, TEST_URL } from "./common"
const DOWNLOAD_DIR = "/downloads/complete"
const TORRENT = {
@@ -10,7 +10,7 @@ const TORRENT = {
context('app', () => {
beforeEach(() => {
- cy.visit(`http://zj9495:zj9495@localhost:8888/transmission-client`)
+ cy.visit(TEST_URL)
cy.verifyConnected()
})
@@ -61,6 +61,8 @@ context('app', () => {
cy.contains('Adding...')
cy.contains(TORRENT.NAME)
cy.contains('Cancel').click()
+ // eslint-disable-next-line cypress/no-unnecessary-waiting
+ cy.wait(3000);
cy.contains(TORRENT.NAME).should('not.exist')
})
diff --git a/cypress/integration/app.spec.js b/cypress/integration/app.spec.js
index cd003d3..537ca67 100644
--- a/cypress/integration/app.spec.js
+++ b/cypress/integration/app.spec.js
@@ -1,8 +1,10 @@
///
+import { TEST_URL } from "./common"
+
context('app', () => {
beforeEach(() => {
- cy.visit(`http://zj9495:zj9495@localhost:8888/transmission-client`)
+ cy.visit(TEST_URL)
})
it('boot', () => {
diff --git a/cypress/integration/common/index.js b/cypress/integration/common/index.js
index 54a9d65..7c4b016 100644
--- a/cypress/integration/common/index.js
+++ b/cypress/integration/common/index.js
@@ -25,4 +25,13 @@ export const removeTestTorrent = () => {
cy.get('[data-testid=delete-local-data] [type=checkbox]').check()
cy.contains('OK').click()
cy.contains(TEST_TORRENT.NAME).should('not.exist')
-};
\ No newline at end of file
+};
+
+export const getUrl = () => {
+ console.log('TEST_URL: ');
+ const { env } = Cypress
+ // result example: protocol://user:password@host:port/path
+ return `${env('protocol')}://${env('user')}:${env('password')}@${env('host')}:${env('port')}${env('path')}`
+}
+
+export const TEST_URL = getUrl()
\ No newline at end of file
diff --git a/cypress/integration/i18n.spec.js b/cypress/integration/i18n.spec.js
index 30690f2..22de3b1 100644
--- a/cypress/integration/i18n.spec.js
+++ b/cypress/integration/i18n.spec.js
@@ -1,10 +1,12 @@
///
+import { TEST_URL } from "./common"
import langs from "../../src/i18n/lang"
+import { STORAGE_KEYS } from "../../src/constants"
context('i18n', () => {
beforeEach(() => {
- cy.visit(`http://zj9495:zj9495@localhost:8888/transmission-client`)
+ cy.visit(TEST_URL)
cy.verifyConnected()
})
@@ -122,4 +124,221 @@ context('i18n', () => {
cy.get('#torrent-table .MuiDataGrid-colCellTitle').contains(item.totalSize)
})
})
+
+ it('test get the locale from the url', () => {
+ const TEST_LANGS = [
+ {
+ codes: ["en", "en_US", "en_AU", "en_CA", "en_GB"],
+ text: "English",
+ name: langs.en['torrent.fields.name'],
+ totalSize: langs.en['torrent.fields.totalSize']
+ },
+ {
+ codes: ["zh", "zh_CN"],
+ text: "中文 - 简体",
+ name: langs.zh_CN['torrent.fields.name'],
+ totalSize: langs.zh_CN['torrent.fields.totalSize']
+ },
+ {
+ codes: ["zh_TW", "zh_HK"],
+ text: "中文 - 繁体",
+ name: langs.zh_TW['torrent.fields.name'],
+ totalSize: langs.zh_TW['torrent.fields.totalSize']
+ },
+ {
+ codes: ["de", "de_DE", "de_AT"],
+ text: "Deutsch",
+ name: langs.de['torrent.fields.name'],
+ totalSize: langs.de['torrent.fields.totalSize']
+ },
+ {
+ codes: ["es", "es_ES", "es_MX"],
+ text: "Español",
+ name: langs.es['torrent.fields.name'],
+ totalSize: langs.es['torrent.fields.totalSize']
+ },
+ {
+ codes: ["fr", "fr_FR", "fr_CA"],
+ text: "Français",
+ name: langs.fr['torrent.fields.name'],
+ totalSize: langs.fr['torrent.fields.totalSize']
+ },
+ {
+ codes: ["hu", "hu_HU"],
+ text: "Magyar",
+ name: langs.hu['torrent.fields.name'],
+ totalSize: langs.hu['torrent.fields.totalSize']
+ },
+ {
+ codes: ["it", "it_IT", "it_CH"],
+ text: "Italiano",
+ name: langs.it['torrent.fields.name'],
+ totalSize: langs.it['torrent.fields.totalSize']
+ },
+ {
+ codes: ["ko", "ko_KR"],
+ text: "Korean",
+ name: langs.ko['torrent.fields.name'],
+ totalSize: langs.ko['torrent.fields.totalSize']
+ },
+ {
+ codes: ["nl", "nl_NL", "nl_BE"],
+ text: "Nederlands",
+ name: langs.nl['torrent.fields.name'],
+ totalSize: langs.nl['torrent.fields.totalSize']
+ },
+ {
+ codes: ["pt_BR"],
+ text: "Português - Brasil",
+ name: langs.pt_BR['torrent.fields.name'],
+ totalSize: langs.pt_BR['torrent.fields.totalSize']
+ },
+ {
+ codes: ["pt_PT"],
+ text: "Português - Portugal",
+ name: langs.pt_PT['torrent.fields.name'],
+ totalSize: langs.pt_PT['torrent.fields.totalSize']
+ },
+ {
+ codes: ["ro", "ro_RO"],
+ text: "Romanian",
+ name: langs.ro['torrent.fields.name'],
+ totalSize: langs.ro['torrent.fields.totalSize']
+ },
+ {
+ codes: ["ru", "ru_RU"],
+ text: "Русский",
+ name: langs.ru['torrent.fields.name'],
+ totalSize: langs.ru['torrent.fields.totalSize']
+ },
+ {
+ codes: ["uk", "uk_UA"],
+ text: "українська мова",
+ name: langs.uk['torrent.fields.name'],
+ totalSize: langs.uk['torrent.fields.totalSize']
+ },
+ ]
+ const url = `${TEST_URL}?locale=`
+ TEST_LANGS.forEach(language => {
+ language.codes.forEach(locale => {
+ cy.visit(`${url}${locale}`)
+ cy.verifyConnected()
+ cy.get('#selected-language').contains(language.text)
+ cy.get('#torrent-table .MuiDataGrid-colCellTitle').contains(language.name)
+ cy.get('#torrent-table .MuiDataGrid-colCellTitle').contains(language.totalSize)
+ })
+ })
+ })
+
+ it('use the browser language as the default language', () => {
+ const TEST_LANGS = [
+ {
+ codes: ["en_US", "en_AU", "en_CA", "en_GB"],
+ text: "English",
+ name: langs.en['torrent.fields.name'],
+ totalSize: langs.en['torrent.fields.totalSize']
+ },
+ {
+ codes: ["zh_CN"],
+ text: "中文 - 简体",
+ name: langs.zh_CN['torrent.fields.name'],
+ totalSize: langs.zh_CN['torrent.fields.totalSize']
+ },
+ {
+ codes: ["zh_TW", "zh_HK"],
+ text: "中文 - 繁体",
+ name: langs.zh_TW['torrent.fields.name'],
+ totalSize: langs.zh_TW['torrent.fields.totalSize']
+ },
+ {
+ codes: ["de_DE", "de_AT"],
+ text: "Deutsch",
+ name: langs.de['torrent.fields.name'],
+ totalSize: langs.de['torrent.fields.totalSize']
+ },
+ {
+ codes: ["es_ES", "es_MX"],
+ text: "Español",
+ name: langs.es['torrent.fields.name'],
+ totalSize: langs.es['torrent.fields.totalSize']
+ },
+ {
+ codes: ["fr_FR", "fr_CA"],
+ text: "Français",
+ name: langs.fr['torrent.fields.name'],
+ totalSize: langs.fr['torrent.fields.totalSize']
+ },
+ {
+ codes: ["hu_HU"],
+ text: "Magyar",
+ name: langs.hu['torrent.fields.name'],
+ totalSize: langs.hu['torrent.fields.totalSize']
+ },
+ {
+ codes: ["it_IT", "it_CH"],
+ text: "Italiano",
+ name: langs.it['torrent.fields.name'],
+ totalSize: langs.it['torrent.fields.totalSize']
+ },
+ {
+ codes: ["ko_KR"],
+ text: "Korean",
+ name: langs.ko['torrent.fields.name'],
+ totalSize: langs.ko['torrent.fields.totalSize']
+ },
+ {
+ codes: ["nl_NL", "nl_BE"],
+ text: "Nederlands",
+ name: langs.nl['torrent.fields.name'],
+ totalSize: langs.nl['torrent.fields.totalSize']
+ },
+ {
+ codes: ["pt_BR"],
+ text: "Português - Brasil",
+ name: langs.pt_BR['torrent.fields.name'],
+ totalSize: langs.pt_BR['torrent.fields.totalSize']
+ },
+ {
+ codes: ["pt_PT"],
+ text: "Português - Portugal",
+ name: langs.pt_PT['torrent.fields.name'],
+ totalSize: langs.pt_PT['torrent.fields.totalSize']
+ },
+ {
+ codes: ["ro_RO"],
+ text: "Romanian",
+ name: langs.ro['torrent.fields.name'],
+ totalSize: langs.ro['torrent.fields.totalSize']
+ },
+ {
+ codes: ["ru_RU"],
+ text: "Русский",
+ name: langs.ru['torrent.fields.name'],
+ totalSize: langs.ru['torrent.fields.totalSize']
+ },
+ {
+ codes: ["uk_UA"],
+ text: "українська мова",
+ name: langs.uk['torrent.fields.name'],
+ totalSize: langs.uk['torrent.fields.totalSize']
+ },
+ ]
+ TEST_LANGS.forEach(language => {
+ language.codes.forEach(locale => {
+ cy.visit(TEST_URL, {
+ onBeforeLoad: (window) => {
+ // 'window:before:load:' will be fired before 'onBeforeLoad', so use setTimeout to hack it
+ setTimeout(() => {
+ Object.defineProperty(window.navigator, 'language', { value: locale, configurable: true });
+ window.localStorage.removeItem(STORAGE_KEYS.LOCALE)
+ })
+ }
+ })
+ cy.verifyConnected()
+ cy.get('#selected-language').contains(language.text)
+ cy.get('#torrent-table .MuiDataGrid-colCellTitle').contains(language.name)
+ cy.get('#torrent-table .MuiDataGrid-colCellTitle').contains(language.totalSize)
+ })
+ })
+ })
})
\ No newline at end of file
diff --git a/cypress/integration/list.spec.js b/cypress/integration/list.spec.js
index d83a44e..cf6a52f 100644
--- a/cypress/integration/list.spec.js
+++ b/cypress/integration/list.spec.js
@@ -1,20 +1,37 @@
///
-import { removeTestTorrent, addTestTorrent } from "./common"
+import { removeTestTorrent, addTestTorrent, TEST_URL } from "./common"
import { TEST_TORRENT } from "../fixtures/constants"
-context('test torrent list', () => {
+context("test torrent list", () => {
beforeEach(() => {
- cy.visit(`http://zj9495:zj9495@localhost:8888/transmission-client`)
+ cy.visit(TEST_URL)
cy.verifyConnected()
- addTestTorrent(false)
})
- afterEach(() => {
+ it("should format ratio if ratio < 0", () => {
+ addTestTorrent(false)
+
+ cy.contains(TEST_TORRENT.NAME).closest(".MuiDataGrid-row").find("[data-field=uploadRatio]").contains("0")
+
removeTestTorrent()
})
- it('should format ratio if ratio < 0', () => {
- cy.contains(TEST_TORRENT.NAME).closest('.MuiDataGrid-row').find('[data-field=uploadRatio]').contains('0')
+ it("test the default display columns", () => {
+ const COLUMNS = {
+ DISPLAY: ["name", "totalSize", "percentDone", "leftUntilDone", "uploadRatio", "status", "seederCount", "leecherCount", "rateDownload", "rateUpload", "addedDate", "downloadDir"],
+ HIDE: ["completeSize", "uploadedEver", "queuePosition", "trackers", "activityDate", "labels", "doneDate"]
+ }
+
+ cy.viewport(1920, 1080)
+
+ COLUMNS.DISPLAY.forEach(name => {
+ // should be display
+ cy.get(`[data-field=${name}]`).should("be.exist")
+ })
+ COLUMNS.HIDE.forEach(name => {
+ // should not be display
+ cy.get(`[data-field=${name}]`).should("not.exist")
+ })
})
})
\ No newline at end of file
diff --git a/cypress/integration/removeTorrents.spec.js b/cypress/integration/removeTorrents.spec.js
index 15d360b..08d48f9 100644
--- a/cypress/integration/removeTorrents.spec.js
+++ b/cypress/integration/removeTorrents.spec.js
@@ -1,11 +1,11 @@
///
-import { addTestTorrent } from "./common"
+import { addTestTorrent, TEST_URL } from "./common"
import { TEST_TORRENT } from "../fixtures/constants"
context('app', () => {
beforeEach(() => {
- cy.visit(`http://zj9495:zj9495@localhost:8888/transmission-client`)
+ cy.visit(TEST_URL)
cy.verifyConnected()
})
@@ -21,4 +21,20 @@ context('app', () => {
cy.contains('Successfully removed!')
cy.contains(TEST_TORRENT.NAME).should('not.exist')
})
+
+ it('should clear the selection after by multiple delete', () => {
+ addTestTorrent()
+
+ cy.contains(TEST_TORRENT.NAME).closest('.MuiDataGrid-row').find('input[type=checkbox]').check()
+ cy.get('.MuiDataGrid-columnsContainer input[type=checkbox]').should('be.checked')
+
+ cy.getByTestId('delete-btn').click()
+ cy.contains('Remove confirm')
+ cy.get('[data-testid=delete-local-data] [type=checkbox]').check()
+ cy.contains('OK').click()
+ cy.contains('Successfully removed!')
+
+ cy.get('.MuiDataGrid-columnsContainer input[type=checkbox]').should('not.checked')
+ cy.getByTestId('delete-btn').should('be.disabled')
+ })
})
\ No newline at end of file
diff --git a/cypress/integration/theme.spec.js b/cypress/integration/theme.spec.js
new file mode 100644
index 0000000..8c2ea31
--- /dev/null
+++ b/cypress/integration/theme.spec.js
@@ -0,0 +1,69 @@
+/* eslint-disable @typescript-eslint/no-empty-function */
+///
+
+import { TEST_URL } from "./common"
+
+context('app', () => {
+ beforeEach(() => {
+ cy.visit(TEST_URL)
+ cy.verifyConnected()
+ })
+
+ it('should use the auto theme at first boot', () => {
+ cy.clearLocalStorage();
+ cy.reload();
+ cy.getByTestId("theme-toggle-button").find("[data-test-id=auto]").should('be.exist');
+ })
+
+ it('should use the last theme when reloading', () => {
+ cy.clearLocalStorage();
+ cy.reload();
+
+ cy.getByTestId("theme-toggle-button").click();
+ cy.getByTestId("theme-toggle-button").find("[data-test-id=light]").should('be.exist');
+ cy.reload();
+ cy.getByTestId("theme-toggle-button").find("[data-test-id=light]").should('be.exist');
+
+ cy.getByTestId("theme-toggle-button").click();
+ cy.getByTestId("theme-toggle-button").find("[data-test-id=dark]").should('be.exist');
+ cy.reload();
+ cy.getByTestId("theme-toggle-button").find("[data-test-id=dark]").should('be.exist');
+ })
+
+ it('should use the system theme on the auto theme', () => {
+ const DAKR_THEME_SWITCH = {
+ ENABLED: true,
+ DISABLED: false
+ }
+
+ cy.clearLocalStorage();
+ cy.visit(TEST_URL, {
+ onBeforeLoad: (window) => {
+ cy.stub(window, 'matchMedia')
+ .withArgs('(prefers-color-scheme: dark)')
+ .returns({
+ matches: DAKR_THEME_SWITCH.ENABLED,
+ addEventListener: () => {}
+ })
+ }
+ })
+
+ cy.getByTestId("theme-toggle-button").find("[data-test-id=auto]").should('be.exist');
+ cy.get("body").invoke("css", "background-color").should("eq", "rgb(48, 48, 48)");
+
+ cy.clearLocalStorage();
+ cy.visit(TEST_URL, {
+ onBeforeLoad: (window) => {
+ cy.stub(window, 'matchMedia')
+ .withArgs('(prefers-color-scheme: dark)')
+ .returns({
+ matches: DAKR_THEME_SWITCH.DISABLED,
+ addEventListener: () => {}
+ })
+ }
+ })
+
+ cy.getByTestId("theme-toggle-button").find("[data-test-id=auto]").should('be.exist');
+ cy.get("body").invoke("css", "background-color").should("eq", "rgb(250, 250, 250)");
+ })
+})
\ No newline at end of file
diff --git a/cypress/support/index.js b/cypress/support/index.js
index d68db96..cc45fd7 100644
--- a/cypress/support/index.js
+++ b/cypress/support/index.js
@@ -18,3 +18,7 @@ import './commands'
// Alternatively you can use CommonJS syntax:
// require('./commands')
+
+Cypress.on('window:before:load', window => {
+ Object.defineProperty(window.navigator, 'language', { value: 'en-US', configurable: true });
+});
\ No newline at end of file
diff --git a/package.json b/package.json
index 6af0354..4463a35 100644
--- a/package.json
+++ b/package.json
@@ -24,12 +24,14 @@
"axios": "^0.21.0",
"clsx": "^1.1.1",
"cypress": "^6.2.1",
+ "http-proxy-middleware": "^2.0.0",
"husky": "^4.3.7",
"immer": "^8.0.0",
"lint-staged": "^10.5.3",
"lodash": "^4.17.20",
"moment": "^2.29.1",
"prettier": "^2.2.1",
+ "query-string": "^7.0.0",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-hook-form": "^6.14.1",
@@ -45,7 +47,6 @@
"typescript": "^4.0.3",
"web-vitals": "^0.2.4"
},
- "proxy": "http://tr.zj9495.com",
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
@@ -82,7 +83,8 @@
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-react": "^7.22.0",
"eslint-plugin-react-hooks": "^4.2.0",
- "eslint-plugin-unicorn": "^26.0.1"
+ "eslint-plugin-unicorn": "^26.0.1",
+ "jest-matchmedia-mock": "^1.1.0"
},
"husky": {
"hooks": {
diff --git a/src/appStart.ts b/src/appStart.ts
index 4d3e5a4..d079a84 100644
--- a/src/appStart.ts
+++ b/src/appStart.ts
@@ -1,12 +1,51 @@
+/* eslint-disable class-methods-use-this */
import { LicenseInfo } from "@material-ui/x-grid";
+import store from "src/store";
+import { setLocale, toggleTheme } from "src/store/actions/rpc";
+import { DEFAULT_LANGUAGE, STORAGE_KEYS } from "src/constants";
+import { formatLocale } from "src/utils/formatter";
+import qs from "query-string";
+
class AppStart {
- // eslint-disable-next-line class-methods-use-this
setupLicense() {
LicenseInfo.setLicenseKey(process.env.REACT_APP_MUIX_LICENSE_KEY as string);
}
+ setupLocale() {
+ const localeFromQs = qs.parse(window.location.search).locale as string;
+ const localeFromStorage = window.localStorage[STORAGE_KEYS.LOCALE];
+ const localeFromStore = store.getState().rpc.locale;
+ const localeFromNavigator = formatLocale(navigator.language, false);
+ const locale =
+ formatLocale(localeFromQs, false) ||
+ localeFromStore ||
+ localeFromStorage ||
+ localeFromNavigator ||
+ DEFAULT_LANGUAGE.code;
+ setLocale(locale)(store.dispatch);
+ if (localeFromQs) {
+ const url = window.location.href.replace(`locale=${localeFromQs}`, "");
+
+ window.history.replaceState(
+ {
+ url,
+ title: "",
+ },
+ "",
+ url
+ );
+ }
+ }
+ setupTheme() {
+ const themeFromStorage = window.localStorage[STORAGE_KEYS.THEME];
+ if (themeFromStorage) {
+ toggleTheme(themeFromStorage)(store.dispatch, store.getState);
+ }
+ }
start() {
this.setupLicense();
+ this.setupLocale();
+ this.setupTheme();
}
}
diff --git a/src/components/Header/index.tsx b/src/components/Header/index.tsx
index 9e7ed18..1f596c7 100644
--- a/src/components/Header/index.tsx
+++ b/src/components/Header/index.tsx
@@ -16,11 +16,10 @@ import {
import SearchIcon from "@material-ui/icons/Search";
import GitHubIcon from "@material-ui/icons/GitHub";
import MenuIcon from "@material-ui/icons/Menu";
-import { useDispatch, useSelector } from "react-redux";
+import { useDispatch } from "react-redux";
import { useIntl } from "react-intl";
import { toggleMenuOpen } from "src/store/actions/rpc";
-import { IState } from "src/types";
import { GITHUB_REPO } from "src/constants";
import ThemeToggle from "src/components/ThemeToggle";
import LanguageToggle from "src/components/LanguageToggle";
@@ -32,6 +31,8 @@ const useStyles = makeStyles((theme: Theme) =>
},
appBar: {
zIndex: 1201,
+ backgroundColor:
+ theme.palette.type === "dark" ? "transparent" : undefined,
},
menuButton: {
marginRight: theme.spacing(2),
@@ -98,21 +99,13 @@ export default function SearchAppBar() {
const dispatch = useDispatch();
const intl = useIntl();
- const themeType = useSelector((state: IState) => state.rpc.theme);
-
- const appBarColor = themeType === "dark" ? "transparent" : undefined;
-
const handleMenuClick = () => {
dispatch(toggleMenuOpen());
};
return (
-
+
{
) : undefined
}
>
- {message}
+ {message}
>
diff --git a/src/components/RemoveTorrentDialog/index.tsx b/src/components/RemoveTorrentDialog/index.tsx
index bcf9bf9..699cc1e 100644
--- a/src/components/RemoveTorrentDialog/index.tsx
+++ b/src/components/RemoveTorrentDialog/index.tsx
@@ -23,6 +23,7 @@ import {
toggleRemoveTorrentsDialog,
setMessageBar,
} from "src/store/actions/app";
+import { setSelectedIds } from "src/store/actions/rpc";
import { removeTorrents } from "src/api";
interface IFormInput {
@@ -71,7 +72,10 @@ const AddTorrentDialog = () => {
})
);
removeTorrents(selectIds, data.deleteLocalData)
- .then((result) => handleResult(result.data as TResult))
+ .then((result) => {
+ handleResult(result.data as TResult);
+ dispatch(setSelectedIds([]));
+ })
.catch(() => {
dispatch(
setMessageBar({
diff --git a/src/components/ThemeToggle/index.tsx b/src/components/ThemeToggle/index.tsx
index 3144335..387ee1d 100644
--- a/src/components/ThemeToggle/index.tsx
+++ b/src/components/ThemeToggle/index.tsx
@@ -23,12 +23,13 @@ const ThemeToggle = () => {
- {theme === "light" && }
- {theme === "dark" && }
- {theme === "auto" && }
+ {theme === "light" && }
+ {theme === "dark" && }
+ {theme === "auto" && }
);
diff --git a/src/components/TorrentTable/index.tsx b/src/components/TorrentTable/index.tsx
index e359d72..3504736 100644
--- a/src/components/TorrentTable/index.tsx
+++ b/src/components/TorrentTable/index.tsx
@@ -13,7 +13,7 @@ import { makeStyles, Theme, createStyles } from "@material-ui/core/styles";
import { useParams } from "react-router-dom";
-import { getTorrents } from "src/store/selector";
+import { getTorrents, getSelectedIds } from "src/store/selector";
import { setSelectedIds } from "src/store/actions/rpc";
import {
formatSize,
@@ -135,8 +135,22 @@ const columns: GridColDef[] = [
) as unknown) as string,
...useSpeed,
},
+ {
+ field: "addedDate",
+ headerName: ((
+
+ ) as unknown) as string,
+ ...useTime,
+ },
+ {
+ field: "downloadDir",
+ headerName: ((
+
+ ) as unknown) as string,
+ },
{
field: "completeSize",
+ hide: true,
headerName: ((
) as unknown) as string,
@@ -144,38 +158,29 @@ const columns: GridColDef[] = [
},
{
field: "uploadedEver",
+ hide: true,
headerName: ((
) as unknown) as string,
...useSize,
},
- {
- field: "addedDate",
- headerName: ((
-
- ) as unknown) as string,
- ...useTime,
- },
{
field: "queuePosition",
+ hide: true,
headerName: ((
) as unknown) as string,
},
{
field: "trackers",
+ hide: true,
headerName: ((
) as unknown) as string,
},
- {
- field: "downloadDir",
- headerName: ((
-
- ) as unknown) as string,
- },
{
field: "activityDate",
+ hide: true,
headerName: ((
) as unknown) as string,
@@ -183,12 +188,14 @@ const columns: GridColDef[] = [
},
{
field: "labels",
+ hide: true,
headerName: ((
) as unknown) as string,
},
{
field: "doneDate",
+ hide: true,
headerName: ((
) as unknown) as string,
@@ -199,6 +206,7 @@ const columns: GridColDef[] = [
const TorrentTable: React.FC = () => {
const { torrentStatus } = useParams();
const torrents = useSelector(getTorrents);
+ const selectedIds = useSelector(getSelectedIds);
const dispatch = useDispatch();
const classes = useStyles();
@@ -220,6 +228,7 @@ const TorrentTable: React.FC = () => {
}}
rows={rows}
columns={columns}
+ selectionModel={selectedIds}
pageSize={20}
checkboxSelection
disableSelectionOnClick
diff --git a/src/constants.ts b/src/constants.ts
index 43d6723..d58010c 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -1,3 +1,4 @@
+/* eslint-disable global-require */
import { IStatusColor } from "./types";
export const APP_ROUTES = {
@@ -11,65 +12,87 @@ export const LANGUAGES = [
{
code: "en",
text: "English",
+ langFile: require("src/i18n/lang/en.json"),
},
{
code: "zh-CN",
text: "中文 - 简体",
+ langFile: require("src/i18n/lang/zh_CN.json"),
},
{
code: "zh-TW",
text: "中文 - 繁体",
+ langFile: require("src/i18n/lang/zh_TW.json"),
},
{
code: "de",
text: "Deutsch",
+ langFile: require("src/i18n/lang/de.json"),
},
{
code: "es",
text: "Español",
+ langFile: require("src/i18n/lang/es.json"),
},
{
code: "fr",
text: "Français",
+ langFile: require("src/i18n/lang/fr.json"),
},
{
code: "hu",
text: "Magyar",
+ langFile: require("src/i18n/lang/hu.json"),
},
{
code: "it",
text: "Italiano",
+ langFile: require("src/i18n/lang/it.json"),
},
{
code: "ko",
text: "Korean",
+ langFile: require("src/i18n/lang/ko.json"),
},
{
code: "nl",
text: "Nederlands",
+ langFile: require("src/i18n/lang/nl.json"),
+ },
+ {
+ code: "pl",
+ text: "Polska - Polski",
+ langFile: require("src/i18n/lang/pl.json"),
},
{
code: "pt-BR",
text: "Português - Brasil",
+ langFile: require("src/i18n/lang/pt_BR.json"),
},
{
code: "pt-PT",
text: "Português - Portugal",
+ langFile: require("src/i18n/lang/pt_PT.json"),
},
{
code: "ro",
text: "Romanian",
+ langFile: require("src/i18n/lang/ro.json"),
},
{
code: "ru",
text: "Русский",
+ langFile: require("src/i18n/lang/ru.json"),
},
{
code: "uk",
text: "українська мова",
+ langFile: require("src/i18n/lang/uk.json"),
},
];
+export const DEFAULT_LANGUAGE = LANGUAGES[0];
+
export const GITHUB_REPO = "https://github.com/zj9495/transmission-client";
export const STATUS_COLORS: IStatusColor = {
@@ -92,3 +115,8 @@ export const PRIORITY_NORMAL = 0;
export const PRIORITY_LOW = -1;
export const INVALID_STATUS = -1;
+
+export const STORAGE_KEYS = {
+ LOCALE: "LOCALE",
+ THEME: "THEME",
+};
diff --git a/src/containers/Intl/index.tsx b/src/containers/Intl/index.tsx
index 4611696..722aaed 100644
--- a/src/containers/Intl/index.tsx
+++ b/src/containers/Intl/index.tsx
@@ -1,9 +1,10 @@
-/* eslint-disable import/no-dynamic-require */
import React from "react";
import { IntlProvider } from "react-intl";
import { connect } from "react-redux";
+import { find } from "lodash";
import { IState } from "src/types";
+import { LANGUAGES, DEFAULT_LANGUAGE } from "src/constants";
interface Props {
locale: string;
@@ -11,15 +12,12 @@ interface Props {
}
function getMessages(locale: string): Record {
- let messages: Record;
- try {
- // eslint-disable-next-line global-require
- messages = require(`src/i18n/lang/${locale.replace("-", "_")}.json`);
- } catch {
- // eslint-disable-next-line global-require
- messages = require(`src/i18n/lang/en.json`);
- }
- return messages;
+ const messages = find(LANGUAGES, { code: locale })?.langFile || {};
+ const defaultMessages = DEFAULT_LANGUAGE.langFile;
+ return {
+ ...defaultMessages,
+ ...messages,
+ };
}
const Intl = (props: Props) => {
const { locale, children } = props;
diff --git a/src/containers/Theme/index.tsx b/src/containers/Theme/index.tsx
index 7b35320..064e801 100644
--- a/src/containers/Theme/index.tsx
+++ b/src/containers/Theme/index.tsx
@@ -1,13 +1,13 @@
-import React from "react";
+import React, { useEffect, useState } from "react";
import { createMuiTheme, ThemeProvider } from "@material-ui/core/styles";
-import { connect } from "react-redux";
+import { useSelector } from "react-redux";
import { enUS, zhCN, Localization } from "@material-ui/core/locale";
+import { blue } from "@material-ui/core/colors";
-import { IState } from "src/types";
+import { Theme as ITheme } from "src/types";
+import { getLocale, getTheme } from "src/store/selector";
interface Props {
- locale: string;
- themeType: "light" | "dark" | "auto";
children?: JSX.Element;
}
@@ -26,8 +26,31 @@ function getMessages(locale: string): Localization {
return muiLocale;
}
+const getThemeType = (theme: ITheme) => {
+ if (theme === "auto") {
+ return window.matchMedia("(prefers-color-scheme: dark)").matches
+ ? "dark"
+ : "light";
+ }
+ return theme;
+};
+
const Theme = (props: Props) => {
- const { locale, themeType, children } = props;
+ const locale = useSelector(getLocale);
+ const themeFromStore = useSelector(getTheme);
+ const [themeType, setThemeType] = useState(getThemeType(themeFromStore));
+ useEffect(() => {
+ setThemeType(getThemeType(themeFromStore));
+ }, [themeFromStore]);
+ useEffect(() => {
+ const media = window.matchMedia("(prefers-color-scheme: dark)");
+ const callback = () => {
+ setThemeType(getThemeType(themeFromStore));
+ };
+ media.addEventListener("change", callback);
+ return () => media.removeEventListener("change", callback);
+ }, []);
+ const { children } = props;
const messages = getMessages(locale);
const theme = createMuiTheme(
{
@@ -35,7 +58,11 @@ const Theme = (props: Props) => {
fontSize: 12,
},
palette: {
- type: themeType === "auto" ? undefined : themeType,
+ type: themeType,
+ primary: {
+ main: themeType === "light" ? blue[700] : blue[200],
+ contrastText: "#fff",
+ },
},
},
messages
@@ -44,9 +71,4 @@ const Theme = (props: Props) => {
return {children};
};
-const mapStateToProps = (state: IState) => ({
- locale: state.rpc.locale,
- themeType: state.rpc.theme,
-});
-
-export default connect(mapStateToProps)(Theme);
+export default Theme;
diff --git a/src/setupProxy.js b/src/setupProxy.js
new file mode 100644
index 0000000..e0bf513
--- /dev/null
+++ b/src/setupProxy.js
@@ -0,0 +1,21 @@
+/* eslint-disable @typescript-eslint/no-var-requires */
+const { createProxyMiddleware } = require("http-proxy-middleware");
+
+const proxyDelay = function (req, res, next) {
+ const delay = Number(process.env.REACT_APP_RPC_DELAY);
+ if (delay) {
+ setTimeout(next, delay);
+ } else {
+ next();
+ }
+};
+module.exports = function (app) {
+ app.use(
+ "/transmission/rpc",
+ proxyDelay,
+ createProxyMiddleware({
+ target: process.env.REACT_APP_TRANSMISSION_RPC,
+ changeOrigin: true,
+ })
+ );
+};
diff --git a/src/setupTests.ts b/src/setupTests.ts
index 1dd407a..8e41c7d 100644
--- a/src/setupTests.ts
+++ b/src/setupTests.ts
@@ -3,3 +3,8 @@
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import "@testing-library/jest-dom";
+// eslint-disable-next-line import/no-extraneous-dependencies
+import MatchMediaMock from "jest-matchmedia-mock";
+
+// eslint-disable-next-line no-new
+new MatchMediaMock();
diff --git a/src/store/actions/rpc.ts b/src/store/actions/rpc.ts
index f1eadd6..f988aa5 100644
--- a/src/store/actions/rpc.ts
+++ b/src/store/actions/rpc.ts
@@ -1,8 +1,8 @@
/* eslint-disable @typescript-eslint/ban-types */
import { ThunkDispatch } from "redux-thunk";
import { AnyAction } from "redux";
-import { STATUS_TYPES } from "src/constants";
-import { ITorrent, ITorrents, IState } from "src/types";
+import { STATUS_TYPES, STORAGE_KEYS } from "src/constants";
+import { ITorrent, ITorrents, IState, Theme } from "src/types";
import { getSession, getTorrents, getSessionStats } from "src/api";
import { objectToCamelCase } from "src/utils/object";
@@ -23,18 +23,26 @@ export const setLocale = (val: string) => (
type: SET_LOCALE,
payload: val,
});
+ window.localStorage.setItem(STORAGE_KEYS.LOCALE, val);
};
-export const toggleTheme = () => (
+export const toggleTheme = (theme?: Theme) => (
dispatch: ThunkDispatch<{}, {}, AnyAction>,
getState: () => IState
) => {
+ const nextThemeMap: Record = {
+ light: "dark",
+ dark: "auto",
+ auto: "light",
+ };
const state = getState();
- const payload = state.rpc.theme === "light" ? "dark" : "light";
+ const currentTheme: Theme = state.rpc.theme;
+ const nextTheme: Theme = theme || nextThemeMap[currentTheme];
dispatch({
type: CHANGE_THEME,
- payload,
+ payload: nextTheme,
});
+ window.localStorage.setItem(STORAGE_KEYS.THEME, nextTheme);
};
export const getSessionAction = () => (
diff --git a/src/store/reducers/rpc.ts b/src/store/reducers/rpc.ts
index 4ae1236..e24472b 100644
--- a/src/store/reducers/rpc.ts
+++ b/src/store/reducers/rpc.ts
@@ -13,8 +13,8 @@ import {
} from "../constants";
export const initialRPCState: IRPCState = {
- locale: "en",
- theme: "light",
+ locale: "",
+ theme: "auto",
sessionId: undefined,
torrents: {
all: [],
diff --git a/src/store/selector.ts b/src/store/selector.ts
index c66b087..363b782 100644
--- a/src/store/selector.ts
+++ b/src/store/selector.ts
@@ -1,6 +1,7 @@
import { IState } from "src/types";
export const getLocale = (state: IState) => state.rpc.locale;
+export const getTheme = (state: IState) => state.rpc.theme;
export const getSessionSelector = (state: IState) => state.rpc.session;
export const getDownloadDirSelector = (state: IState) =>
diff --git a/src/types.ts b/src/types.ts
index 922615b..04faa35 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -64,9 +64,11 @@ export interface IStats {
uploadSpeed: number;
}
+export type Theme = "light" | "dark" | "auto";
+
export interface IRPCState {
locale: string;
- theme: "light" | "dark" | "auto";
+ theme: Theme;
sessionId: string | undefined;
torrents: ITorrents;
session: ISession;
diff --git a/src/utils/formatter.test.ts b/src/utils/formatter.test.ts
index 6655e0b..4b56b32 100644
--- a/src/utils/formatter.test.ts
+++ b/src/utils/formatter.test.ts
@@ -5,6 +5,7 @@ import {
formatSpeed,
formatUnixTimeStamp,
formatLeftTime,
+ formatLocale,
} from "./formatter";
describe.each`
@@ -166,3 +167,122 @@ describe.each`
expect(formatLeftTime(ms)).toBe(expected);
});
});
+
+describe.each`
+ locale | expected
+ ${"en"} | ${"en"}
+ ${"en-US"} | ${"en"}
+ ${"en-AU"} | ${"en"}
+ ${"en-CA"} | ${"en"}
+ ${"en-GB"} | ${"en"}
+ ${"de"} | ${"de"}
+ ${"de-DE"} | ${"de"}
+ ${"es"} | ${"es"}
+ ${"es-ES"} | ${"es"}
+ ${"es-PR"} | ${"es"}
+ ${"fr"} | ${"fr"}
+ ${"fr-FR"} | ${"fr"}
+ ${"fr-CA"} | ${"fr"}
+ ${"hu"} | ${"hu"}
+ ${"hu-HU"} | ${"hu"}
+ ${"it"} | ${"it"}
+ ${"it-IT"} | ${"it"}
+ ${"it-CH"} | ${"it"}
+ ${"ko"} | ${"ko"}
+ ${"ko-KR"} | ${"ko"}
+ ${"nl"} | ${"nl"}
+ ${"nl-BE"} | ${"nl"}
+ ${"nl-NL"} | ${"nl"}
+ ${"pl"} | ${"pl"}
+ ${"pt"} | ${"pt-BR"}
+ ${"pt-BR"} | ${"pt-BR"}
+ ${"pt-PT"} | ${"pt-PT"}
+ ${"ro"} | ${"ro"}
+ ${"ro-RO"} | ${"ro"}
+ ${"ru"} | ${"ru"}
+ ${"ru-RU"} | ${"ru"}
+ ${"uk"} | ${"uk"}
+ ${"uk-UA"} | ${"uk"}
+ ${"zh"} | ${"zh-CN"}
+ ${"zh-CN"} | ${"zh-CN"}
+ ${"zh-TW"} | ${"zh-TW"}
+ ${"zh-HK"} | ${"zh-TW"}
+`("test formatLocale($locale)", ({ locale, expected }) => {
+ test(`returns ${expected}`, () => {
+ expect(formatLocale(locale)).toBe(expected);
+ });
+});
+
+describe.each`
+ locale | expected
+ ${"en_US"} | ${"en"}
+ ${"en_AU"} | ${"en"}
+ ${"en_CA"} | ${"en"}
+ ${"en_GB"} | ${"en"}
+ ${"de_DE"} | ${"de"}
+ ${"es_ES"} | ${"es"}
+ ${"es_PR"} | ${"es"}
+ ${"fr_FR"} | ${"fr"}
+ ${"fr_CA"} | ${"fr"}
+ ${"hu_HU"} | ${"hu"}
+ ${"it_IT"} | ${"it"}
+ ${"it_CH"} | ${"it"}
+ ${"ko_KR"} | ${"ko"}
+ ${"nl_BE"} | ${"nl"}
+ ${"nl_NL"} | ${"nl"}
+ ${"pt_BR"} | ${"pt-BR"}
+ ${"pt_PT"} | ${"pt-PT"}
+ ${"zh_CN"} | ${"zh-CN"}
+ ${"zh_TW"} | ${"zh-TW"}
+ ${"zh_HK"} | ${"zh-TW"}
+`("should correctly formatLocale($locale) with _", ({ locale, expected }) => {
+ test(`returns ${expected}`, () => {
+ expect(formatLocale(locale)).toBe(expected);
+ });
+});
+
+describe.each`
+ unsupportedLoacle | expected
+ ${"af"} | ${"en"}
+ ${"af-ZA"} | ${"en"}
+ ${"sq-AL"} | ${"en"}
+ ${"ar"} | ${"en"}
+ ${"ar-DZ"} | ${"en"}
+ ${"hy-AM"} | ${"en"}
+ ${"eu-ES"} | ${"en"}
+`(
+ "should return `en` when formatLocale($unsupportedLoacle)",
+ ({ unsupportedLoacle, expected }) => {
+ test(`returns ${expected}`, () => {
+ expect(formatLocale(unsupportedLoacle)).toBe(expected);
+ });
+ }
+);
+
+describe.each`
+ locale | useDefaultLang | expected
+ ${undefined} | ${false} | ${undefined}
+ ${undefined} | ${true} | ${"en"}
+ ${undefined} | ${undefined} | ${"en"}
+ ${"af"} | ${false} | ${undefined}
+ ${"af"} | ${true} | ${"en"}
+ ${"af-ZA"} | ${false} | ${undefined}
+ ${"af-ZA"} | ${true} | ${"en"}
+ ${"sq-AL"} | ${false} | ${undefined}
+ ${"sq-AL"} | ${true} | ${"en"}
+ ${"ar"} | ${false} | ${undefined}
+ ${"ar"} | ${true} | ${"en"}
+ ${"ar-DZ"} | ${false} | ${undefined}
+ ${"ar-DZ"} | ${true} | ${"en"}
+ ${"hy-AM"} | ${false} | ${undefined}
+ ${"hy-AM"} | ${true} | ${"en"}
+ ${"eu-ES"} | ${false} | ${undefined}
+ ${"eu-ES"} | ${true} | ${"en"}
+`(
+ "test formatLocale($locale, $useDefaultLang) with useDefaultLang param",
+ ({ locale, useDefaultLang, expected }) => {
+ test(`returns ${expected}`, () => {
+ expect(formatLocale(locale, useDefaultLang)).toBe(expected);
+ });
+ }
+);
diff --git a/src/utils/formatter.ts b/src/utils/formatter.ts
index ef50f18..d5ec517 100644
--- a/src/utils/formatter.ts
+++ b/src/utils/formatter.ts
@@ -1,6 +1,8 @@
import moment from "moment";
+import { find } from "lodash";
import store from "src/store";
+import { LANGUAGES, DEFAULT_LANGUAGE } from "src/constants";
export const formatBytes = (
number: number,
@@ -60,3 +62,25 @@ export const formatLeftTime = (second: number): string => {
return `${hours}:${minutes}:${seconds}`;
};
+
+export const formatLocale = (
+ locale = "",
+ useDefaultLang = true
+): string | undefined => {
+ if (!locale && !useDefaultLang) {
+ // eslint-disable-next-line unicorn/no-useless-undefined
+ return undefined;
+ }
+ if (["zh_HK", "zh-HK"].includes(locale)) {
+ return "zh-TW";
+ }
+ const [lang, country] = locale.split(/-|_/);
+ locale = `${lang}-${country}`;
+ let localeConfig =
+ find(LANGUAGES, (o) => o.code === lang || o.code === locale) ||
+ find(LANGUAGES, (o) => o.code.startsWith(lang));
+ if (useDefaultLang && !localeConfig) {
+ localeConfig = DEFAULT_LANGUAGE;
+ }
+ return localeConfig?.code;
+};
diff --git a/yarn.lock b/yarn.lock
index dc02654..bfee678 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2770,6 +2770,13 @@
resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-5.1.1.tgz#3c9ee980f1a10d6021ae6632ca3e79ca2ec4fb50"
integrity sha512-giAlZwstKbmvMk1OO7WXSj4OZ0keXAcl2TQq4LWHiiPH2ByaH7WeUzng+Qej8UPxxv+8lRTuouo0iaNDBuzIBA==
+"@types/http-proxy@^1.17.5":
+ version "1.17.6"
+ resolved "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.6.tgz#62dc3fade227d6ac2862c8f19ee0da9da9fd8616"
+ integrity sha512-+qsjqR75S/ib0ig0R9WN+CDoZeOBU6F2XLewgC4KVgdXiNHiKKHFEMRHOrs5PbYE97D5vataw5wPj4KLYfUkuQ==
+ dependencies:
+ "@types/node" "*"
+
"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1":
version "2.0.3"
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz#4ba8ddb720221f432e443bd5f9117fd22cfd4762"
@@ -6682,6 +6689,11 @@ fill-range@^7.0.1:
dependencies:
to-regex-range "^5.0.1"
+filter-obj@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz#9b311112bc6c6127a16e016c6c5d7f19e0805c5b"
+ integrity sha1-mzERErxsYSehbgFsbF1/GeCAXFs=
+
finalhandler@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d"
@@ -7404,7 +7416,18 @@ http-proxy-middleware@0.19.1:
lodash "^4.17.11"
micromatch "^3.1.10"
-http-proxy@^1.17.0:
+http-proxy-middleware@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.0.tgz#20d1ac3409199c83e5d0383ba6436b04e7acb9fe"
+ integrity sha512-S+RN5njuyvYV760aiVKnyuTXqUMcSIvYOsHA891DOVQyrdZOwaXtBHpt9FUVPEDAsOvsPArZp6VXQLs44yvkow==
+ dependencies:
+ "@types/http-proxy" "^1.17.5"
+ http-proxy "^1.18.1"
+ is-glob "^4.0.1"
+ is-plain-obj "^3.0.0"
+ micromatch "^4.0.2"
+
+http-proxy@^1.17.0, http-proxy@^1.18.1:
version "1.18.1"
resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549"
integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==
@@ -7944,6 +7967,11 @@ is-plain-obj@^1.0.0, is-plain-obj@^1.1.0:
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e"
integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4=
+is-plain-obj@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz#af6f2ea14ac5a646183a5bbdb5baabbc156ad9d7"
+ integrity sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==
+
is-plain-object@^2.0.3, is-plain-object@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677"
@@ -8314,6 +8342,11 @@ jest-matcher-utils@^26.6.0, jest-matcher-utils@^26.6.2:
jest-get-type "^26.3.0"
pretty-format "^26.6.2"
+jest-matchmedia-mock@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/jest-matchmedia-mock/-/jest-matchmedia-mock-1.1.0.tgz#eaae8c5d1dee4e4f7c59f8cb1b38b5d7ea842552"
+ integrity sha512-REnJRsOSCMpGAlkxmvVTqEBpregyFVi9MPEH3N83W1yLKzDdNehtCkcdDZduXq74PLtfI+11NyM4zKCK5ynV9g==
+
jest-message-util@^26.6.0, jest-message-util@^26.6.2:
version "26.6.2"
resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-26.6.2.tgz#58173744ad6fc0506b5d21150b9be56ef001ca07"
@@ -11182,6 +11215,16 @@ query-string@^4.1.0:
object-assign "^4.1.0"
strict-uri-encode "^1.0.0"
+query-string@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.npmjs.org/query-string/-/query-string-7.0.0.tgz#aaad2c8d5c6a6d0c6afada877fecbd56af79e609"
+ integrity sha512-Iy7moLybliR5ZgrK/1R3vjrXq03S13Vz4Rbm5Jg3EFq1LUmQppto0qtXz4vqZ386MSRjZgnTSZ9QC+NZOSd/XA==
+ dependencies:
+ decode-uri-component "^0.2.0"
+ filter-obj "^1.1.0"
+ split-on-first "^1.0.0"
+ strict-uri-encode "^2.0.0"
+
querystring-es3@^0.2.0:
version "0.2.1"
resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73"
@@ -12508,6 +12551,11 @@ spdy@^4.0.2:
select-hose "^2.0.0"
spdy-transport "^3.0.0"
+split-on-first@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz#f610afeee3b12bce1d0c30425e76398b78249a5f"
+ integrity sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==
+
split-string@^3.0.1, split-string@^3.0.2:
version "3.1.0"
resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2"
@@ -12628,6 +12676,11 @@ strict-uri-encode@^1.0.0:
resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713"
integrity sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=
+strict-uri-encode@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546"
+ integrity sha1-ucczDHBChi9rFC3CdLvMWGbONUY=
+
string-argv@0.3.1:
version "0.3.1"
resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.1.tgz#95e2fbec0427ae19184935f816d74aaa4c5c19da"