diff options
| author | Peter Liu <30622363+PedroLiu1999@users.noreply.github.com> | 2026-04-20 01:41:50 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-04-20 01:41:50 +0300 |
| commit | 36b2a586756fc279304170976c9d486498d55919 (patch) | |
| tree | 71a1dd665fa14a0fc9dad28c3ef421e3e3d7fe7c /web | |
| parent | 59e98592251cac1dedb93905cb368a3ba93ad19c (diff) | |
feat: Add NordVPN NordLynx (WireGuard) integration (#3827)
* feat: Add NordVPN NordLynx (WireGuard) integration with dedicated UI and backend services.
* remove limit=10 to get all servers
* feat: add city selector to NordVPN modal
* feat: auto-select best server on country/city change
* feat: simplify filter logic and enforce > 7% load
* fix
---------
Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
Diffstat (limited to 'web')
| -rw-r--r-- | web/controller/xray_setting.go | 28 | ||||
| -rw-r--r-- | web/html/modals/nord_modal.html | 306 | ||||
| -rw-r--r-- | web/html/settings/xray/basics.html | 19 | ||||
| -rw-r--r-- | web/html/settings/xray/outbounds.html | 2 | ||||
| -rw-r--r-- | web/html/xray.html | 22 | ||||
| -rw-r--r-- | web/service/nord.go | 145 | ||||
| -rw-r--r-- | web/service/setting.go | 9 | ||||
| -rw-r--r-- | web/translation/translate.ar_EG.toml | 12 | ||||
| -rw-r--r-- | web/translation/translate.en_US.toml | 12 | ||||
| -rw-r--r-- | web/translation/translate.es_ES.toml | 12 | ||||
| -rw-r--r-- | web/translation/translate.fa_IR.toml | 12 | ||||
| -rw-r--r-- | web/translation/translate.id_ID.toml | 12 | ||||
| -rw-r--r-- | web/translation/translate.ja_JP.toml | 12 | ||||
| -rw-r--r-- | web/translation/translate.pt_BR.toml | 12 | ||||
| -rw-r--r-- | web/translation/translate.ru_RU.toml | 14 | ||||
| -rw-r--r-- | web/translation/translate.tr_TR.toml | 12 | ||||
| -rw-r--r-- | web/translation/translate.uk_UA.toml | 12 | ||||
| -rw-r--r-- | web/translation/translate.vi_VN.toml | 12 | ||||
| -rw-r--r-- | web/translation/translate.zh_CN.toml | 12 | ||||
| -rw-r--r-- | web/translation/translate.zh_TW.toml | 12 |
20 files changed, 688 insertions, 1 deletions
diff --git a/web/controller/xray_setting.go b/web/controller/xray_setting.go index 5b7a0e26..0c382fb9 100644 --- a/web/controller/xray_setting.go +++ b/web/controller/xray_setting.go @@ -17,6 +17,7 @@ type XraySettingController struct { OutboundService service.OutboundService XrayService service.XrayService WarpService service.WarpService + NordService service.NordService } // NewXraySettingController creates a new XraySettingController and initializes its routes. @@ -35,6 +36,7 @@ func (a *XraySettingController) initRouter(g *gin.RouterGroup) { g.POST("/", a.getXraySetting) g.POST("/warp/:action", a.warp) + g.POST("/nord/:action", a.nord) g.POST("/update", a.updateSetting) g.POST("/resetOutboundsTraffic", a.resetOutboundsTraffic) g.POST("/testOutbound", a.testOutbound) @@ -123,6 +125,32 @@ func (a *XraySettingController) warp(c *gin.Context) { jsonObj(c, resp, err) } +// nord handles NordVPN-related operations based on the action parameter. +func (a *XraySettingController) nord(c *gin.Context) { + action := c.Param("action") + var resp string + var err error + switch action { + case "countries": + resp, err = a.NordService.GetCountries() + case "servers": + countryId := c.PostForm("countryId") + resp, err = a.NordService.GetServers(countryId) + case "reg": + token := c.PostForm("token") + resp, err = a.NordService.GetCredentials(token) + case "setKey": + key := c.PostForm("key") + resp, err = a.NordService.SetKey(key) + case "data": + resp, err = a.NordService.GetNordData() + case "del": + err = a.NordService.DelNordData() + } + + jsonObj(c, resp, err) +} + // getOutboundsTraffic retrieves the traffic statistics for outbounds. func (a *XraySettingController) getOutboundsTraffic(c *gin.Context) { outboundsTraffic, err := a.OutboundService.GetOutboundsTraffic() diff --git a/web/html/modals/nord_modal.html b/web/html/modals/nord_modal.html new file mode 100644 index 00000000..c86d8ed9 --- /dev/null +++ b/web/html/modals/nord_modal.html @@ -0,0 +1,306 @@ +{{define "modals/nordModal"}} +<a-modal id="nord-modal" v-model="nordModal.visible" title="NordVPN NordLynx" + :confirm-loading="nordModal.confirmLoading" :closable="true" :mask-closable="true" + :footer="null" :class="themeSwitcher.currentTheme"> + <template v-if="nordModal.nordData == null"> + <a-tabs default-active-key="token" :class="themeSwitcher.currentTheme"> + <a-tab-pane key="token" tab='{{ i18n "pages.xray.outbound.accessToken" }}'> + <a-form :colon="false" :label-col="{ md: {span:6} }" :wrapper-col="{ md: {span:18} }" :style="{ marginTop: '20px' }"> + <a-form-item label='{{ i18n "pages.xray.outbound.accessToken" }}'> + <a-input v-model="nordModal.token" placeholder='{{ i18n "pages.xray.outbound.accessToken" }}'></a-input> + <div :style="{ marginTop: '10px' }"> + <a-button type="primary" icon="login" @click="login()" :loading="nordModal.confirmLoading">{{ i18n "login" }}</a-button> + </div> + </a-form-item> + </a-form> + </a-tab-pane> + <a-tab-pane key="key" tab='{{ i18n "pages.xray.outbound.privateKey" }}'> + <a-form :colon="false" :label-col="{ md: {span:6} }" :wrapper-col="{ md: {span:18} }" :style="{ marginTop: '20px' }"> + <a-form-item label='{{ i18n "pages.xray.outbound.privateKey" }}'> + <a-input v-model="nordModal.manualKey" placeholder='{{ i18n "pages.xray.outbound.privateKey" }}'></a-input> + <div :style="{ marginTop: '10px' }"> + <a-button type="primary" icon="save" @click="saveKey()" :loading="nordModal.confirmLoading">{{ i18n "save" }}</a-button> + </div> + </a-form-item> + </a-form> + </a-tab-pane> + </a-tabs> + </template> + <template v-else> + <table :style="{ margin: '5px 0', width: '100%' }"> + <tr class="client-table-odd-row" v-if="nordModal.nordData.token"> + <td>{{ i18n "pages.xray.outbound.accessToken" }}</td> + <td>[[ nordModal.nordData.token ]]</td> + </tr> + <tr> + <td>{{ i18n "pages.xray.outbound.privateKey" }}</td> + <td>[[ nordModal.nordData.private_key ]]</td> + </tr> + </table> + <a-button @click="logout" :loading="nordModal.confirmLoading" type="danger">{{ i18n "logout" }}</a-button> + <a-divider :style="{ margin: '0' }">{{ i18n "pages.xray.outbound.settings" }}</a-divider> + <a-form :colon="false" :label-col="{ md: {span:6} }" :wrapper-col="{ md: {span:18} }" :style="{ marginTop: '10px' }"> + <a-form-item label='{{ i18n "pages.xray.outbound.country" }}'> + <a-select v-model="nordModal.countryId" @change="fetchServers" show-search option-filter-prop="label"> + <a-select-option v-for="c in nordModal.countries" :key="c.id" :value="c.id" :label="c.name"> + [[ c.name ]] ([[ c.code ]]) + </a-select-option> + </a-select> + </a-form-item> + <a-form-item label='{{ i18n "pages.xray.outbound.city" }}' v-if="nordModal.cities.length > 0"> + <a-select v-model="nordModal.cityId" @change="onCityChange" show-search option-filter-prop="label"> + <a-select-option :key="0" :value="null" label='{{ i18n "pages.xray.outbound.allCities" }}'> + {{ i18n "pages.xray.outbound.allCities" }} + </a-select-option> + <a-select-option v-for="c in nordModal.cities" :key="c.id" :value="c.id" :label="c.name"> + [[ c.name ]] + </a-select-option> + </a-select> + </a-form-item> + <a-form-item label='{{ i18n "pages.xray.outbound.server" }}' v-if="filteredServers.length > 0"> + <a-select v-model="nordModal.serverId"> + <a-select-option v-for="s in filteredServers" :key="s.id" :value="s.id"> + [[ s.cityName ]] - [[ s.name ]] ({{ i18n "pages.xray.outbound.load" }}: [[ s.load ]]%) + </a-select-option> + </a-select> + </a-form-item> + </a-form> + <a-divider :style="{ margin: '10px 0' }">{{ i18n "pages.xray.outbound.outboundStatus" }}</a-divider> + <a-form :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }"> + <template v-if="nordOutboundIndex>=0"> + <a-tag color="green" :style="{ lineHeight: '31px' }">{{ i18n "enabled" }}</a-tag> + <a-button @click="resetOutbound" :loading="nordModal.confirmLoading" type="danger">{{ i18n "reset" }}</a-button> + </template> + <template v-else> + <a-tag color="orange" :style="{ lineHeight: '31px' }">{{ i18n "disabled" }}</a-tag> + <a-button @click="addOutbound" :disabled="!nordModal.serverId" :loading="nordModal.confirmLoading" type="primary">{{ i18n "pages.xray.outbound.addOutbound" }}</a-button> + </template> + </a-form> + </template> +</a-modal> + +<script> + const nordModal = { + visible: false, + confirmLoading: false, + nordData: null, + token: '', + manualKey: '', + countries: [], + countryId: null, + cities: [], + cityId: null, + servers: [], + serverId: null, + show() { + this.visible = true; + this.getData(); + }, + close() { + this.visible = false; + }, + loading(loading = true) { + this.confirmLoading = loading; + }, + async getData() { + this.loading(true); + const msg = await HttpUtil.post('/panel/xray/nord/data'); + if (msg.success) { + this.nordData = msg.obj ? JSON.parse(msg.obj) : null; + if (this.nordData) { + await this.fetchCountries(); + } + } + this.loading(false); + }, + async login() { + this.loading(true); + const msg = await HttpUtil.post('/panel/xray/nord/reg', { token: this.token }); + if (msg.success) { + this.nordData = JSON.parse(msg.obj); + await this.fetchCountries(); + } + this.loading(false); + }, + async saveKey() { + this.loading(true); + const msg = await HttpUtil.post('/panel/xray/nord/setKey', { key: this.manualKey }); + if (msg.success) { + this.nordData = JSON.parse(msg.obj); + await this.fetchCountries(); + } + this.loading(false); + }, + async logout(index) { + this.loading(true); + const msg = await HttpUtil.post('/panel/xray/nord/del'); + if (msg.success) { + this.delOutbound(index); + this.delRouting(); + this.nordData = null; + this.token = ''; + this.manualKey = ''; + this.countries = []; + this.cities = []; + this.servers = []; + this.countryId = null; + this.cityId = null; + } + this.loading(false); + }, + async fetchCountries() { + const msg = await HttpUtil.post('/panel/xray/nord/countries'); + if (msg.success) { + this.countries = JSON.parse(msg.obj); + } + }, + async fetchServers() { + this.loading(true); + this.servers = []; + this.cities = []; + this.serverId = null; + this.cityId = null; + const msg = await HttpUtil.post('/panel/xray/nord/servers', { countryId: this.countryId }); + if (msg.success) { + const data = JSON.parse(msg.obj); + const locations = data.locations || []; + const locToCity = {}; + const citiesMap = new Map(); + locations.forEach(loc => { + if (loc.country && loc.country.city) { + citiesMap.set(loc.country.city.id, loc.country.city); + locToCity[loc.id] = loc.country.city; + } + }); + this.cities = Array.from(citiesMap.values()).sort((a, b) => a.name.localeCompare(b.name)); + + this.servers = (data.servers || []).map(s => { + const firstLocId = (s.location_ids || [])[0]; + const city = locToCity[firstLocId]; + s.cityId = city ? city.id : null; + s.cityName = city ? city.name : 'Unknown'; + return s; + }).sort((a, b) => a.load - b.load); + + if (this.servers.length > 0) { + this.serverId = this.servers[0].id; + } + + if (this.servers.length === 0) { + app.$message.warning('No servers found for the selected country'); + } + } + this.loading(false); + }, + addOutbound() { + const server = this.servers.find(s => s.id === this.serverId); + if (!server) return; + + const tech = server.technologies.find(t => t.id === 35); + const publicKey = tech.metadata.find(m => m.name === 'public_key').value; + + const outbound = { + tag: `nord-${server.hostname}`, + protocol: 'wireguard', + settings: { + secretKey: this.nordData.private_key, + address: ['10.5.0.2/32'], + peers: [{ + publicKey: publicKey, + endpoint: server.station + ':51820' + }], + noKernelTun: false + } + }; + + app.templateSettings.outbounds.push(outbound); + app.outboundSettings = JSON.stringify(app.templateSettings.outbounds); + this.close(); + app.$message.success('NordVPN outbound added'); + }, + resetOutbound(index) { + const server = this.servers.find(s => s.id === this.serverId); + if (!server || index === -1) return; + + const tech = server.technologies.find(t => t.id === 35); + const publicKey = tech.metadata.find(m => m.name === 'public_key').value; + + const oldTag = app.templateSettings.outbounds[index].tag; + const newTag = `nord-${server.hostname}`; + + const outbound = { + tag: newTag, + protocol: 'wireguard', + settings: { + secretKey: this.nordData.private_key, + address: ['10.5.0.2/32'], + peers: [{ + publicKey: publicKey, + endpoint: server.station + ':51820' + }], + noKernelTun: false + } + }; + app.templateSettings.outbounds[index] = outbound; + + // Sync routing rules + app.templateSettings.routing.rules.forEach(r => { + if (r.outboundTag === oldTag) { + r.outboundTag = newTag; + } + }); + + app.outboundSettings = JSON.stringify(app.templateSettings.outbounds); + this.close(); + app.$message.success('NordVPN outbound updated'); + }, + delOutbound(index) { + if (index !== -1) { + app.templateSettings.outbounds.splice(index, 1); + app.outboundSettings = JSON.stringify(app.templateSettings.outbounds); + } + }, + delRouting() { + if (app.templateSettings && app.templateSettings.routing) { + app.templateSettings.routing.rules = app.templateSettings.routing.rules.filter(r => !r.outboundTag.startsWith("nord-")); + } + } + }; + + new Vue({ + delimiters: ['[[', ']]'], + el: '#nord-modal', + data: { + nordModal: nordModal, + }, + methods: { + login: () => nordModal.login(), + saveKey: () => nordModal.saveKey(), + logout() { nordModal.logout(this.nordOutboundIndex) }, + fetchServers: () => nordModal.fetchServers(), + addOutbound: () => nordModal.addOutbound(), + resetOutbound() { nordModal.resetOutbound(this.nordOutboundIndex) }, + onCityChange() { + if (this.filteredServers.length > 0) { + this.nordModal.serverId = this.filteredServers[0].id; + } else { + this.nordModal.serverId = null; + } + } + }, + computed: { + nordOutboundIndex: { + get: function () { + return app.templateSettings ? app.templateSettings.outbounds.findIndex((o) => o.tag.startsWith("nord-")) : -1; + } + }, + filteredServers: function() { + if (!this.nordModal.cityId) { + return this.nordModal.servers; + } + return this.nordModal.servers.filter(s => s.cityId === this.nordModal.cityId); + } + } + }); +</script> +{{end}} diff --git a/web/html/settings/xray/basics.html b/web/html/settings/xray/basics.html index 9a31038a..c637e30a 100644 --- a/web/html/settings/xray/basics.html +++ b/web/html/settings/xray/basics.html @@ -313,6 +313,25 @@ </template> </template> </a-setting-list-item> + <a-setting-list-item paddings="small"> + <template #title>{{ i18n "pages.xray.nordRouting" }}</template> + <template #control> + <template v-if="NordExist"> + <a-select mode="tags" :style="{ width: '100%' }" + v-model="nordDomains" + :dropdown-class-name="themeSwitcher.currentTheme"> + <a-select-option :value="p.value" :label="p.label" + v-for="p in settingsData.ServicesOptions"> + <span>[[ p.label ]]</span> + </a-select-option> + </a-select> + </template> + <template v-else> + <a-button type="primary" icon="api" + @click="showNord()">{{ i18n "pages.xray.outbound.nordvpn" }}</a-button> + </template> + </template> + </a-setting-list-item> </a-collapse-panel> <a-collapse-panel key="6" header='{{ i18n "pages.settings.resetDefaultConfig"}}'> diff --git a/web/html/settings/xray/outbounds.html b/web/html/settings/xray/outbounds.html index 3995760f..232fe55e 100644 --- a/web/html/settings/xray/outbounds.html +++ b/web/html/settings/xray/outbounds.html @@ -9,6 +9,8 @@ </a-button> <a-button type="primary" icon="cloud" @click="showWarp()">WARP</a-button> + <a-button type="primary" icon="api" + @click="showNord()">NordVPN</a-button> </a-space> </a-col> <a-col :xs="12" :sm="12" :lg="12" :style="{ textAlign: 'right' }"> diff --git a/web/html/xray.html b/web/html/xray.html index 040ae2ee..01b4e4e2 100644 --- a/web/html/xray.html +++ b/web/html/xray.html @@ -163,6 +163,7 @@ {{template "modals/dnsPresetsModal"}} {{template "modals/fakednsModal"}} {{template "modals/warpModal"}} +{{template "modals/nordModal"}} <script> const rulesColumns = [ { title: "#", align: 'center', width: 15, scopedSlots: { customRender: 'action' } }, @@ -1056,6 +1057,9 @@ showWarp() { warpModal.show(); }, + showNord() { + nordModal.show(); + }, async loadCustomGeoAliases() { try { const msg = await HttpUtil.get('/panel/api/custom-geo/aliases'); @@ -1429,6 +1433,19 @@ this.templateRuleSetter({ outboundTag: "warp", property: "domain", data: newValue }); } }, + nordTag: { + get: function () { + return this.templateSettings ? (this.templateSettings.outbounds.find((o) => o.tag.startsWith("nord-")) || { tag: "nord" }).tag : "nord"; + } + }, + nordDomains: { + get: function () { + return this.templateRuleGetter({ outboundTag: this.nordTag, property: "domain" }); + }, + set: function (newValue) { + this.templateRuleSetter({ outboundTag: this.nordTag, property: "domain", data: newValue }); + } + }, torrentSettings: { get: function () { return ArrayUtils.doAllItemsExist(this.settingsData.protocols.bittorrent, this.blockedProtocols); @@ -1446,6 +1463,11 @@ return this.templateSettings ? this.templateSettings.outbounds.findIndex((o) => o.tag == "warp") >= 0 : false; }, }, + NordExist: { + get: function () { + return this.templateSettings ? this.templateSettings.outbounds.findIndex((o) => o.tag.startsWith("nord-")) >= 0 : false; + }, + }, enableDNS: { get: function () { return this.templateSettings ? this.templateSettings.dns != null : false; diff --git a/web/service/nord.go b/web/service/nord.go new file mode 100644 index 00000000..db0a48f9 --- /dev/null +++ b/web/service/nord.go @@ -0,0 +1,145 @@ +package service + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "time" + + "github.com/mhsanaei/3x-ui/v2/util/common" +) + +type NordService struct { + SettingService +} + +var nordHTTPClient = &http.Client{Timeout: 15 * time.Second} + +// maxResponseSize limits the maximum size of NordVPN API responses (10 MB). +const maxResponseSize = 10 << 20 + +func (s *NordService) GetCountries() (string, error) { + resp, err := nordHTTPClient.Get("https://api.nordvpn.com/v1/countries") + if err != nil { + return "", err + } + defer resp.Body.Close() + body, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseSize)) + if err != nil { + return "", err + } + return string(body), nil +} + +func (s *NordService) GetServers(countryId string) (string, error) { + // Validate countryId is numeric to prevent URL injection + for _, c := range countryId { + if c < '0' || c > '9' { + return "", common.NewError("invalid country ID") + } + } + url := fmt.Sprintf("https://api.nordvpn.com/v2/servers?limit=0&filters[servers_technologies][id]=35&filters[country_id]=%s", countryId) + resp, err := nordHTTPClient.Get(url) + if err != nil { + return "", err + } + defer resp.Body.Close() + body, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseSize)) + if err != nil { + return "", err + } + var data map[string]any + if err := json.Unmarshal(body, &data); err != nil { + return string(body), nil + } + + servers, ok := data["servers"].([]any) + if !ok { + return string(body), nil + } + + var filtered []any + for _, s := range servers { + if server, ok := s.(map[string]any); ok { + if load, ok := server["load"].(float64); ok && load > 7 { + filtered = append(filtered, s) + } + } + } + data["servers"] = filtered + + result, _ := json.Marshal(data) + return string(result), nil +} + +func (s *NordService) SetKey(privateKey string) (string, error) { + if privateKey == "" { + return "", common.NewError("private key cannot be empty") + } + nordData := map[string]string{ + "private_key": privateKey, + "token": "", + } + data, _ := json.Marshal(nordData) + err := s.SettingService.SetNord(string(data)) + if err != nil { + return "", err + } + return string(data), nil +} + +func (s *NordService) GetCredentials(token string) (string, error) { + url := "https://api.nordvpn.com/v1/users/services/credentials" + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return "", err + } + req.SetBasicAuth("token", token) + + client := &http.Client{Timeout: 10 * time.Second} + resp, err := client.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", common.NewErrorf("NordVPN API error: %s", resp.Status) + } + + body, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseSize)) + if err != nil { + return "", err + } + + var creds map[string]any + if err := json.Unmarshal(body, &creds); err != nil { + return "", err + } + + privateKey, ok := creds["nordlynx_private_key"].(string) + if !ok || privateKey == "" { + return "", common.NewError("failed to retrieve NordLynx private key") + } + + nordData := map[string]string{ + "private_key": privateKey, + "token": token, + } + data, _ := json.Marshal(nordData) + err = s.SettingService.SetNord(string(data)) + if err != nil { + return "", err + } + + return string(data), nil +} + +func (s *NordService) GetNordData() (string, error) { + return s.SettingService.GetNord() +} + +func (s *NordService) DelNordData() error { + return s.SettingService.SetNord("") +} diff --git a/web/service/setting.go b/web/service/setting.go index 468a7960..04d8f6a8 100644 --- a/web/service/setting.go +++ b/web/service/setting.go @@ -80,6 +80,7 @@ var defaultValueMap = map[string]string{ "subJsonRules": "", "datepicker": "gregorian", "warp": "", + "nord": "", "externalTrafficInformEnable": "false", "externalTrafficInformURI": "", "xrayOutboundTestUrl": "https://www.google.com/generate_204", @@ -598,6 +599,14 @@ func (s *SettingService) SetWarp(data string) error { return s.setString("warp", data) } +func (s *SettingService) GetNord() (string, error) { + return s.getString("nord") +} + +func (s *SettingService) SetNord(data string) error { + return s.setString("nord", data) +} + func (s *SettingService) GetExternalTrafficInformEnable() (bool, error) { return s.getBool("externalTrafficInformEnable") } diff --git a/web/translation/translate.ar_EG.toml b/web/translation/translate.ar_EG.toml index ace753e1..f9216621 100644 --- a/web/translation/translate.ar_EG.toml +++ b/web/translation/translate.ar_EG.toml @@ -4,6 +4,8 @@ "confirm" = "تأكيد" "cancel" = "إلغاء" "close" = "إغلاق" +"save" = "حفظ" +"logout" = "تسجيل خروج" "create" = "إنشاء" "update" = "تحديث" "copy" = "نسخ" @@ -496,6 +498,8 @@ "ipv4RoutingDesc" = "الخيارات دي هتوجه الترافيك بناءً على وجهة معينة عبر IPv4." "warpRouting" = "توجيه WARP" "warpRoutingDesc" = "الخيارات دي هتوجه الترافيك بناءً على وجهة معينة عبر WARP." +"nordRouting" = "توجيه NordVPN" +"nordRoutingDesc" = "الخيارات دي هتوجه الترافيك بناءً على وجهة معينة عبر NordVPN." "Template" = "قالب إعدادات Xray المتقدم" "TemplateDesc" = "ملف إعدادات Xray النهائي هيتولد بناءً على القالب ده." "FreedomStrategy" = "استراتيجية بروتوكول الحرية" @@ -573,6 +577,14 @@ "testSuccess" = "الاختبار ناجح" "testFailed" = "فشل الاختبار" "testError" = "فشل اختبار المخرج" +"nordvpn" = "NordVPN" +"accessToken" = "رمز الوصول" +"country" = "الدولة" +"server" = "الخادم" +"city" = "المدينة" +"allCities" = "كل المدن" +"privateKey" = "المفتاح الخاص" +"load" = "الحمل" [pages.xray.balancer] "addBalancer" = "أضف موازن تحميل" diff --git a/web/translation/translate.en_US.toml b/web/translation/translate.en_US.toml index 4cf1be07..91580a29 100644 --- a/web/translation/translate.en_US.toml +++ b/web/translation/translate.en_US.toml @@ -4,6 +4,8 @@ "confirm" = "Confirm" "cancel" = "Cancel" "close" = "Close" +"save" = "Save" +"logout" = "Log Out" "create" = "Create" "update" = "Update" "copy" = "Copy" @@ -496,6 +498,8 @@ "ipv4RoutingDesc" = "These options will route traffic based on a specific destination via IPv4." "warpRouting" = "WARP Routing" "warpRoutingDesc" = "These options will route traffic based on a specific destination via WARP." +"nordRouting" = "NordVPN Routing" +"nordRoutingDesc" = "These options will route traffic based on a specific destination via NordVPN." "Template" = "Advanced Xray Configuration Template" "TemplateDesc" = "The final Xray config file will be generated based on this template." "FreedomStrategy" = "Freedom Protocol Strategy" @@ -573,6 +577,14 @@ "testSuccess" = "Test successful" "testFailed" = "Test failed" "testError" = "Failed to test outbound" +"nordvpn" = "NordVPN" +"accessToken" = "Access Token" +"country" = "Country" +"server" = "Server" +"city" = "City" +"allCities" = "All Cities" +"privateKey" = "Private Key" +"load" = "Load" [pages.xray.balancer] "addBalancer" = "Add Balancer" diff --git a/web/translation/translate.es_ES.toml b/web/translation/translate.es_ES.toml index 8e76fba3..3dfe3ade 100644 --- a/web/translation/translate.es_ES.toml +++ b/web/translation/translate.es_ES.toml @@ -4,6 +4,8 @@ "confirm" = "Confirmar"
"cancel" = "Cancelar"
"close" = "Cerrar"
+"save" = "Guardar"
+"logout" = "Cerrar Sesión"
"create" = "Crear"
"update" = "Actualizar"
"copy" = "Copiar"
@@ -496,6 +498,8 @@ "ipv4RoutingDesc" = "Estas opciones solo enrutarán a los dominios objetivo a través de IPv4."
"warpRouting" = "Enrutamiento WARP"
"warpRoutingDesc" = "Precaución: Antes de usar estas opciones, instale WARP en modo de proxy socks5 en su servidor siguiendo los pasos en el GitHub del panel. WARP enrutará el tráfico a los sitios web a través de los servidores de Cloudflare."
+"nordRouting" = "Enrutamiento NordVPN"
+"nordRoutingDesc" = "Estas opciones enrutarán el tráfico basado en un destino específico a través de NordVPN."
"Template" = "Plantilla de Configuración de Xray"
"TemplateDesc" = "Genera el archivo de configuración final de Xray basado en esta plantilla."
"FreedomStrategy" = "Configurar Estrategia para el Protocolo Freedom"
@@ -573,6 +577,14 @@ "testSuccess" = "Prueba exitosa"
"testFailed" = "Prueba fallida"
"testError" = "Error al probar la salida"
+"nordvpn" = "NordVPN"
+"accessToken" = "Token de acceso"
+"country" = "País"
+"server" = "Servidor"
+"city" = "Ciudad"
+"allCities" = "Todas las ciudades"
+"privateKey" = "Clave privada"
+"load" = "Carga"
[pages.xray.balancer]
"addBalancer" = "Agregar equilibrador"
diff --git a/web/translation/translate.fa_IR.toml b/web/translation/translate.fa_IR.toml index c6ea9a84..5569239f 100644 --- a/web/translation/translate.fa_IR.toml +++ b/web/translation/translate.fa_IR.toml @@ -4,6 +4,8 @@ "confirm" = "تایید" "cancel" = "انصراف" "close" = "بستن" +"save" = "ذخیره" +"logout" = "خروج" "create" = "ایجاد" "update" = "بهروزرسانی" "copy" = "کپی" @@ -496,6 +498,8 @@ "ipv4RoutingDesc" = "این گزینهها ترافیک را از طریق آیپی نسخه4 سرور، به مقصد هدایت میکند" "warpRouting" = "WARP مسیریابی" "warpRoutingDesc" = "این گزینهها ترافیک را از طریق وارپ کلادفلر به مقصد هدایت میکند" +"nordRouting" = "مسیریابی NordVPN" +"nordRoutingDesc" = "این گزینهها ترافیک را بر اساس مقصد خاص از
|
