diff --git a/client/src/__locales/vi.js b/client/src/__locales/vi.js index 196ccab6..23643ca1 100644 --- a/client/src/__locales/vi.js +++ b/client/src/__locales/vi.js @@ -38,7 +38,7 @@ export default { 'A number of DNS requests to search engines for which Safe Search was enforced': 'Số yêu cầu DNS tới công cụ tìm kiếm đã chuyển thành tìm kiếm an toàn', 'Average processing time': 'Thời gian xử lý trung bình', 'Average time in milliseconds on processing a DNS request': 'Thời gian trung bình cho một yêu cầu DNS tính bằng mili giây', - // Setting + // Settings 'Block domains using filters and hosts files': 'Chặn tên miền sử dụng các bộ lọc và file hosts', 'You can setup blocking rules in the <a href="#filters">Filters</a> settings.': 'Bạn có thể thiết lập quy tắc chặn tại cài đặt <a href="#filters">Bộ lọc</a>.', 'Use AdGuard browsing security web service': 'Sử dụng dịch vụ bảo vệ duyệt web AdGuard', @@ -54,5 +54,63 @@ export default { 'If you keep this field empty, AdGuard Home will use <a href="https://1.1.1.1/" target="_blank">Cloudflare DNS</a> as an upstream. Use tls:// prefix for DNS over TLS servers.': 'Nếu bạn để trống mục này, AdGuard Home sẽ sử dụng <a href="https://1.1.1.1/" target="_blank">Cloudflare DNS</a> để tìm kiếm. Sử dụng tiền tố tls:// cho các máy chủ DNS dựa trên TLS.', 'Test upstreams': 'Kiểm tra', Apply: 'Áp dụng', + // Settings Toasts + 'Disabled filtering': 'Đã tắt chặn quảng cáo', + 'Enabled filtering': 'Đã bật chặn quảng cáo', + 'Disabled safebrowsing': 'Đã tắt bảo vệ duyệt web', + 'Enabled safebrowsing': 'Đã bật bảo vệ duyệt web', + 'Disabled parental control': 'Đã tắt quản lý của phụ huynh', + 'Enabled parental control': 'Đã bật quản lý của phụ huynh', + 'Disabled safe search': 'Đã tắt tìm kiếm an toàn', + 'Enabled safe search': 'Đã bật tìm kiếm an toàn', + // Filters + Enabled: 'Kích hoạt', + Name: 'Tên', + 'Filter URL': 'URL bộ lọc', + 'Rules count': 'Số quy tắc', + 'Last time updated': 'Cập nhật cuối', + Actions: 'Thao tác', + Delete: 'Xoá', + 'Filters and hosts blocklists': 'Danh sách bộ lọc và hosts', + 'AdGuard Home understands basic adblock rules and hosts files syntax.': 'AdGuard home hiểu các quy tắc chặn quảng cáo đơn giản và cú pháp file hosts', + 'No filters added': 'Không có bộ lọc nào được thêm', + 'Add filter': 'Thêm bộ lọc', + Cancel: 'Huỷ', + 'Enter name': 'Nhập tên', + 'Enter URL': 'Nhập URL', + 'Check updates': 'Kiểm tra cập nhật', + 'New filter subscription': 'Đăng ký bộ lọc mới', + 'Enter a valid URL to a filter subscription or a hosts file.': 'Nhập URL hợp lệ của bộ lọc hoặc file hosts', + 'Custom filtering rules': 'Quy tắc lọc tuỳ chỉnh', + 'Enter one rule on a line. You can use either adblock rules or hosts files syntax.': 'Nhập mỗi quy tắc 1 dòng. Có thể sử dụng quy tắc chặn quảng cáo hoặc cú pháp file host', + Examples: 'Ví dụ', + 'block access to the example.org domain and all its subdomains': 'Chặn truy cập tới tên miền example.org và tất cả tên miền con', + 'unblock access to the example.org domain and all its subdomains': 'Không chặn truy cập tới tên miền example.org và tất cả tên miền con', + 'AdGuard Home will now return 127.0.0.1 address for the example.org domain (but not its subdomains).': 'AdGuard Home sẽ phản hồi địa chỉ IP 127.0.0.1 cho tên miền example.org (không áp dụng tên miền con)', + '! Here goes a comment': '! Đây là một chú thích', + 'just a comment': 'Chỉ là một chú thích', + '# Also a comment': '# Cũng là một chú thích', + // Logs + Unblock: 'Bỏ chặn', + Block: 'Chặn', + Time: 'Thời gian', + 'Domain name': 'Tên miền', + Type: 'Loại', + Response: 'Phản hồi', + Empty: 'Rỗng', + 'Show all': 'Hiện tất cả', + 'Show filtered': 'Chỉ hiện đã chặn', + 'No logs found': 'Không có lịch sử truy vấn', + 'Disable log': 'Tắt lịch sử truy vấn', + 'Download log file': 'Tải tập tin lịch sử truy vấn', + Refresh: 'Làm mới', + 'Enable log': 'Bật lịch sử truy vấn', + 'Last 5000 DNS queries': '5000 truy vấn DNS gần nhất', + Previous: 'Trang trước', + Next: 'Trang sau', + 'Loading...': 'Đang tải...', + Page: 'Trang', + of: 'của', + rows: 'hàng', }, }; diff --git a/client/src/components/Filters/UserRules.js b/client/src/components/Filters/UserRules.js index 4bd71a8f..1f47cf9a 100644 --- a/client/src/components/Filters/UserRules.js +++ b/client/src/components/Filters/UserRules.js @@ -1,8 +1,9 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; +import { Trans, withNamespaces } from 'react-i18next'; import Card from '../ui/Card'; -export default class UserRules extends Component { +class UserRules extends Component { handleChange = (e) => { const { value } = e.currentTarget; this.props.handleRulesChange(value); @@ -14,10 +15,11 @@ export default class UserRules extends Component { }; render() { + const { t } = this.props; return ( <Card - title="Custom filtering rules" - subtitle="Enter one rule on a line. You can use either adblock rules or hosts files syntax." + title={ t('Custom filtering rules') } + subtitle={ t('Enter one rule on a line. You can use either adblock rules or hosts files syntax.') } > <form onSubmit={this.handleSubmit}> <textarea className="form-control form-control--textarea-large" value={this.props.userRules} onChange={this.handleChange} /> @@ -27,31 +29,28 @@ export default class UserRules extends Component { type="submit" onClick={this.handleSubmit} > - Apply + <Trans>Apply</Trans> </button> </div> </form> <hr/> <div className="list leading-loose"> - Examples: + <Trans>Examples</Trans>: <ol className="leading-loose"> <li> - <code>||example.org^</code> - block access to the example.org domain - and all its subdomains + <code>||example.org^</code> - { t('block access to the example.org domain and all its subdomains') } </li> <li> - <code> @@||example.org^</code> - unblock access to the example.org - domain and all its subdomains + <code> @@||example.org^</code> - { t('unblock access to the example.org domain and all its subdomains') } </li> <li> - <code>127.0.0.1 example.org</code> - AdGuard Home will now return - 127.0.0.1 address for the example.org domain (but not its subdomains). + <code>127.0.0.1 example.org</code> - { t('AdGuard Home will now return 127.0.0.1 address for the example.org domain (but not its subdomains).') } </li> <li> - <code>! Here goes a comment</code> - just a comment + <code>{ t('! Here goes a comment') }</code> - { t('just a comment') } </li> <li> - <code># Also a comment</code> - just a comment + <code>{ t('# Also a comment') }</code> - { t('just a comment') } </li> </ol> </div> @@ -64,4 +63,7 @@ UserRules.propTypes = { userRules: PropTypes.string, handleRulesChange: PropTypes.func, handleRulesSubmit: PropTypes.func, + t: PropTypes.func, }; + +export default withNamespaces()(UserRules); diff --git a/client/src/components/Filters/index.js b/client/src/components/Filters/index.js index 6f9d41d4..8c4c3679 100644 --- a/client/src/components/Filters/index.js +++ b/client/src/components/Filters/index.js @@ -1,6 +1,7 @@ import React, { Component } from 'react'; import ReactTable from 'react-table'; import PropTypes from 'prop-types'; +import { Trans, withNamespaces } from 'react-i18next'; import Modal from '../ui/Modal'; import PageTitle from '../ui/PageTitle'; import Card from '../ui/Card'; @@ -33,32 +34,32 @@ class Filters extends Component { }; columns = [{ - Header: 'Enabled', + Header: this.props.t('Enabled'), accessor: 'enabled', Cell: this.renderCheckbox, width: 90, className: 'text-center', }, { - Header: 'Name', + Header: this.props.t('Name'), accessor: 'name', Cell: ({ value }) => (<div className="logs__row logs__row--overflow"><span className="logs__text" title={value}>{value}</span></div>), }, { - Header: 'Filter URL', + Header: this.props.t('Filter URL'), accessor: 'url', Cell: ({ value }) => (<div className="logs__row logs__row--overflow"><a href={value} target='_blank' rel='noopener noreferrer' className="link logs__text">{value}</a></div>), }, { - Header: 'Rules count', + Header: this.props.t('Rules count'), accessor: 'rulesCount', className: 'text-center', Cell: props => props.value.toLocaleString(), }, { - Header: 'Last time updated', + Header: this.props.t('Last time updated'), accessor: 'lastUpdated', className: 'text-center', }, { - Header: 'Actions', + Header: this.props.t('Actions'), accessor: 'url', - Cell: ({ value }) => (<span className='remove-icon fe fe-trash-2' onClick={() => this.props.removeFilter(value)}/>), + Cell: ({ value }) => (<span title={ this.props.t('Delete') } className='remove-icon fe fe-trash-2' onClick={() => this.props.removeFilter(value)}/>), className: 'text-center', width: 75, sortable: false, @@ -66,27 +67,28 @@ class Filters extends Component { ]; render() { + const { t } = this.props; const { filters, userRules } = this.props.filtering; return ( <div> - <PageTitle title="Filters" /> + <PageTitle title={ t('Filters') } /> <div className="content"> <div className="row"> <div className="col-md-12"> <Card - title="Filters and hosts blocklists" - subtitle="AdGuard Home understands basic adblock rules and hosts files syntax." + title={ t('Filters and hosts blocklists') } + subtitle={ t('AdGuard Home understands basic adblock rules and hosts files syntax.') } > <ReactTable data={filters} columns={this.columns} showPagination={false} - noDataText="No filters added" + noDataText={ t('No filters added') } minRows={4} // TODO find out what to show if rules.length is 0 /> <div className="card-actions"> - <button className="btn btn-success btn-standart mr-2" type="submit" onClick={this.props.toggleFilteringModal}>Add filter</button> - <button className="btn btn-primary btn-standart" type="submit" onClick={this.props.refreshFilters}>Check updates</button> + <button className="btn btn-success btn-standart mr-2" type="submit" onClick={this.props.toggleFilteringModal}><Trans>Add filter</Trans></button> + <button className="btn btn-primary btn-standart" type="submit" onClick={this.props.refreshFilters}><Trans>Check updates</Trans></button> </div> </Card> </div> @@ -104,8 +106,8 @@ class Filters extends Component { toggleModal={this.props.toggleFilteringModal} addFilter={this.props.addFilter} isFilterAdded={this.props.filtering.isFilterAdded} - title="New filter subscription" - inputDescription="Enter a valid URL to a filter subscription or a hosts file." + title={ t('New filter subscription') } + inputDescription={ t('Enter a valid URL to a filter subscription or a hosts file.') } /> </div> ); @@ -127,7 +129,8 @@ Filters.propTypes = { toggleFilteringModal: PropTypes.func.isRequired, handleRulesChange: PropTypes.func.isRequired, refreshFilters: PropTypes.func.isRequired, + t: PropTypes.func, }; -export default Filters; +export default withNamespaces()(Filters); diff --git a/client/src/components/Logs/index.js b/client/src/components/Logs/index.js index b533f190..a49783e4 100644 --- a/client/src/components/Logs/index.js +++ b/client/src/components/Logs/index.js @@ -4,6 +4,7 @@ import ReactTable from 'react-table'; import { saveAs } from 'file-saver/FileSaver'; import escapeRegExp from 'lodash/escapeRegExp'; import endsWith from 'lodash/endsWith'; +import { Trans, withNamespaces } from 'react-i18next'; import { formatTime } from '../../helpers/helpers'; import { getTrackerData } from '../../helpers/trackers/trackers'; @@ -75,21 +76,22 @@ class Logs extends Component { className={`btn btn-sm ${buttonClass}`} onClick={() => this.toggleBlocking(buttonText.toLowerCase(), domain)} > - {buttonText} + <Trans>{buttonText}</Trans> </button> </div> ); } renderLogs(logs) { + const { t } = this.props; const columns = [{ - Header: 'Time', + Header: t('Time'), accessor: 'time', maxWidth: 110, filterable: false, Cell: ({ value }) => (<div className="logs__row"><span className="logs__text" title={value}>{formatTime(value)}</span></div>), }, { - Header: 'Domain name', + Header: t('Domain name'), accessor: 'domain', Cell: (row) => { const response = row.value; @@ -105,11 +107,11 @@ class Logs extends Component { ); }, }, { - Header: 'Type', + Header: t('Type'), accessor: 'type', maxWidth: 60, }, { - Header: 'Response', + Header: t('Response'), accessor: 'response', Cell: (row) => { const responses = row.value; @@ -142,7 +144,7 @@ class Logs extends Component { return ( <div className="logs__row"> {this.renderTooltip(isFiltered, rule)} - <span>Empty</span> + <span><Trans>Empty</Trans></span> </div> ); }, @@ -159,11 +161,11 @@ class Logs extends Component { className="form-control" value={filter ? filter.value : 'all'} > - <option value="all">Show all</option> - <option value="filtered">Show filtered</option> + <option value="all">{ t('Show all') }</option> + <option value="filtered">{ t('Show filtered') }</option> </select>, }, { - Header: 'Client', + Header: t('Client'), accessor: 'client', maxWidth: 250, Cell: (row) => { @@ -191,7 +193,14 @@ class Logs extends Component { showPagination={true} defaultPageSize={50} minRows={7} - noDataText="No logs found" + // Text + previousText={ t('Previous') } + nextText={ t('Next') } + loadingText={ t('Loading...') } + pageText={ t('Page') } + ofText={ t('of') } + rowsText={ t('rows') } + noDataText={ t('No logs found') } defaultFilterMethod={(filter, row) => { const id = filter.pivotId || filter.id; return row[id] !== undefined ? @@ -233,17 +242,17 @@ class Logs extends Component { className="btn btn-gray btn-sm mr-2" type="submit" onClick={() => this.props.toggleLogStatus(queryLogEnabled)} - >Disable log</button> + ><Trans>Disable log</Trans></button> <button className="btn btn-primary btn-sm mr-2" type="submit" onClick={this.handleDownloadButton} - >Download log file</button> + ><Trans>Download log file</Trans></button> <button className="btn btn-outline-primary btn-sm" type="submit" onClick={this.getLogs} - >Refresh</button> + ><Trans>Refresh</Trans></button> </Fragment> ); } @@ -253,16 +262,16 @@ class Logs extends Component { className="btn btn-success btn-sm mr-2" type="submit" onClick={() => this.props.toggleLogStatus(queryLogEnabled)} - >Enable log</button> + ><Trans>Enable log</Trans></button> ); } render() { - const { queryLogs, dashboard } = this.props; + const { queryLogs, dashboard, t } = this.props; const { queryLogEnabled } = dashboard; return ( <Fragment> - <PageTitle title="Query Log" subtitle="Last 5000 DNS queries"> + <PageTitle title={ t('Query Log') } subtitle={ t('Last 5000 DNS queries') }> <div className="page-title__actions"> {this.renderButtons(queryLogEnabled)} </div> @@ -288,6 +297,7 @@ Logs.propTypes = { userRules: PropTypes.string, setRules: PropTypes.func, addSuccessToast: PropTypes.func, + t: PropTypes.func, }; -export default Logs; +export default withNamespaces()(Logs); diff --git a/client/src/components/Toasts/Toast.js b/client/src/components/Toasts/Toast.js index 9d85f048..36e40467 100644 --- a/client/src/components/Toasts/Toast.js +++ b/client/src/components/Toasts/Toast.js @@ -1,5 +1,6 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; +import { Trans } from 'react-i18next'; class Toast extends Component { componentDidMount() { @@ -18,7 +19,7 @@ class Toast extends Component { return ( <div className={`toast toast--${this.props.type}`}> <p className="toast__content"> - {this.props.message} + <Trans>{this.props.message}</Trans> </p> <button className="toast__dismiss" onClick={() => this.props.removeToast(this.props.id)}> <svg stroke="#fff" fill="none" width="20" height="20" strokeWidth="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m18 6-12 12"/><path d="m6 6 12 12"/></svg> diff --git a/client/src/components/ui/Modal.js b/client/src/components/ui/Modal.js index cc7cca25..4906d50a 100644 --- a/client/src/components/ui/Modal.js +++ b/client/src/components/ui/Modal.js @@ -2,6 +2,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import ReactModal from 'react-modal'; import classnames from 'classnames'; +import { Trans, withNamespaces } from 'react-i18next'; import { R_URL_REQUIRES_PROTOCOL } from '../../helpers/constants'; import './Modal.css'; @@ -13,7 +14,7 @@ const initialState = { isUrlValid: false, }; -export default class Modal extends Component { +class Modal extends Component { state = initialState; // eslint-disable-next-line @@ -70,8 +71,8 @@ export default class Modal extends Component { if (!this.props.isFilterAdded) { return ( <React.Fragment> - <input type="text" className={inputNameClass} placeholder="Enter name" onChange={this.handleNameChange} /> - <input type="text" className={inputUrlClass} placeholder="Enter URL" onChange={this.handleUrlChange} /> + <input type="text" className={inputNameClass} placeholder={ this.props.t('Enter name') } onChange={this.handleNameChange} /> + <input type="text" className={inputUrlClass} placeholder={ this.props.t('Enter URL') } onChange={this.handleUrlChange} /> {inputDescription && <div className="description"> {inputDescription} @@ -81,7 +82,7 @@ export default class Modal extends Component { } return ( <div className="description"> - Url added successfully + <Trans>Url added successfully</Trans> </div> ); }; @@ -110,8 +111,8 @@ export default class Modal extends Component { { !this.props.isFilterAdded && <div className="modal-footer"> - <button type="button" className="btn btn-secondary" onClick={this.closeModal}>Cancel</button> - <button type="button" className="btn btn-success" onClick={this.handleNext} disabled={isValidForSubmit}>Add filter</button> + <button type="button" className="btn btn-secondary" onClick={this.closeModal}><Trans>Cancel</Trans></button> + <button type="button" className="btn btn-success" onClick={this.handleNext} disabled={isValidForSubmit}><Trans>Add filter</Trans></button> </div> } </div> @@ -127,4 +128,7 @@ Modal.propTypes = { inputDescription: PropTypes.string, addFilter: PropTypes.func.isRequired, isFilterAdded: PropTypes.bool, + t: PropTypes.func, }; + +export default withNamespaces()(Modal);