diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f90e03106..ff0b8e27f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -109,7 +109,7 @@ jobs: with: # Comment out the below 'repository' line if you want to build from # your fork instead of the author's. - repository: lucent-sea/Remotely + repository: vincywindy/Remotely fetch-depth: 0 # Test the Server URL to make sure it's valid diff --git a/Server/API/CultureController.cs b/Server/API/CultureController.cs new file mode 100644 index 000000000..2e412773c --- /dev/null +++ b/Server/API/CultureController.cs @@ -0,0 +1,22 @@ +using Microsoft.AspNetCore.Localization; +using Microsoft.AspNetCore.Mvc; + +namespace Remotely.Server.API +{ + [Route("[controller]/[action]")] + public class CultureController : Controller + { + public IActionResult Set(string culture, string redirectUri) + { + if (culture != null) + { + HttpContext.Response.Cookies.Append( + CookieRequestCultureProvider.DefaultCookieName, + CookieRequestCultureProvider.MakeCookieValue( + new RequestCulture(culture, culture))); + } + + return LocalRedirect(redirectUri); + } + } +} diff --git a/Server/Areas/Identity/Pages/Account/ConfirmEmail.cshtml b/Server/Areas/Identity/Pages/Account/ConfirmEmail.cshtml index beb1acbde..dc4aa3b41 100644 --- a/Server/Areas/Identity/Pages/Account/ConfirmEmail.cshtml +++ b/Server/Areas/Identity/Pages/Account/ConfirmEmail.cshtml @@ -1,7 +1,9 @@ @page @model ConfirmEmailModel +@using Microsoft.Extensions.Localization +@inject IStringLocalizer Localizer @{ - ViewData["Title"] = "Confirm email"; + ViewData["Title"] = Localizer["Confirm email"]; }

@ViewData["Title"]

diff --git a/Server/Areas/Identity/Pages/Account/ConfirmEmailChange.cshtml b/Server/Areas/Identity/Pages/Account/ConfirmEmailChange.cshtml index cbe527582..da4781b34 100644 --- a/Server/Areas/Identity/Pages/Account/ConfirmEmailChange.cshtml +++ b/Server/Areas/Identity/Pages/Account/ConfirmEmailChange.cshtml @@ -1,7 +1,9 @@ @page @model ConfirmEmailChangeModel +@using Microsoft.Extensions.Localization +@inject IStringLocalizer Localizer @{ - ViewData["Title"] = "Confirm email change"; + ViewData["Title"] = Localizer["Confirm email change"]; }

@ViewData["Title"]

diff --git a/Server/Areas/Identity/Pages/Account/ForgotPassword.cshtml b/Server/Areas/Identity/Pages/Account/ForgotPassword.cshtml index 94f46b286..f1f1270de 100644 --- a/Server/Areas/Identity/Pages/Account/ForgotPassword.cshtml +++ b/Server/Areas/Identity/Pages/Account/ForgotPassword.cshtml @@ -1,22 +1,24 @@ @page @model ForgotPasswordModel +@using Microsoft.Extensions.Localization +@inject IStringLocalizer Localizer @{ - ViewData["Title"] = "Forgot your password?"; + ViewData["Title"] = Localizer["Forgot your password?"]; }

@ViewData["Title"]

-

Enter your email.

+

@Localizer["Enter your email."]


- +
- +
diff --git a/Server/Areas/Identity/Pages/Account/ForgotPassword.cshtml.cs b/Server/Areas/Identity/Pages/Account/ForgotPassword.cshtml.cs index 48eb78c96..01d8315b1 100644 --- a/Server/Areas/Identity/Pages/Account/ForgotPassword.cshtml.cs +++ b/Server/Areas/Identity/Pages/Account/ForgotPassword.cshtml.cs @@ -37,7 +37,7 @@ public ForgotPasswordModel(UserManager userManager, public class InputModel { [Required] - [EmailAddress] + [EmailAddress(ErrorMessage = "The true field is not a valid e-mail address.")] public string Email { get; set; } } diff --git a/Server/Areas/Identity/Pages/Account/Login.cshtml b/Server/Areas/Identity/Pages/Account/Login.cshtml index 70bc0160c..6fd6c9bcf 100644 --- a/Server/Areas/Identity/Pages/Account/Login.cshtml +++ b/Server/Areas/Identity/Pages/Account/Login.cshtml @@ -1,8 +1,9 @@ @page @model LoginModel - +@using Microsoft.Extensions.Localization +@inject IStringLocalizer Localizer @{ - ViewData["Title"] = "Log in"; + ViewData["Title"] = Localizer["Log in"]; }

@ViewData["Title"]

@@ -10,16 +11,16 @@
-

Use a local account to log in.

+

@Localizer["Use a local account to log in."]


- +
- +
@@ -27,22 +28,22 @@
- +

- Forgot your password? + @Localizer["Forgot your password?"]

- Register as a new user + @Localizer["Register as a new user"]

- Resend email confirmation + @Localizer["Resend email confirmation"]

diff --git a/Server/Areas/Identity/Pages/Account/Login.cshtml.cs b/Server/Areas/Identity/Pages/Account/Login.cshtml.cs index 6d7c6326f..e5fd27c7b 100644 --- a/Server/Areas/Identity/Pages/Account/Login.cshtml.cs +++ b/Server/Areas/Identity/Pages/Account/Login.cshtml.cs @@ -50,7 +50,7 @@ public LoginModel(SignInManager signInManager, public class InputModel { [Required] - [EmailAddress] + [EmailAddress(ErrorMessage = "The true field is not a valid e-mail address.")] public string Email { get; set; } [Required] diff --git a/Server/Areas/Identity/Pages/Account/Manage/EnableAuthenticator.cshtml b/Server/Areas/Identity/Pages/Account/Manage/EnableAuthenticator.cshtml index abc8074f5..f111abd01 100644 --- a/Server/Areas/Identity/Pages/Account/Manage/EnableAuthenticator.cshtml +++ b/Server/Areas/Identity/Pages/Account/Manage/EnableAuthenticator.cshtml @@ -1,45 +1,46 @@ @page @model EnableAuthenticatorModel +@using Microsoft.Extensions.Localization +@inject IStringLocalizer Localizer @{ - ViewData["Title"] = "Configure authenticator app"; + ViewData["Title"] = Localizer["Configure authenticator app"]; ViewData["ActivePage"] = ManageNavPages.TwoFactorAuthentication; }

@ViewData["Title"]

-

To use an authenticator app go through the following steps:

+

@Localizer["To use an authenticator app go through the following steps:"]

  1. - Download a two-factor authenticator app like Microsoft Authenticator for + @Localizer["Download a two-factor authenticator app like Microsoft Authenticator for"] Android and iOS or - Google Authenticator for + @Localizer["Google Authenticator for"] Android and iOS.

  2. -

    Scan the QR Code or enter this key @Model.SharedKey into your two factor authenticator app. Spaces and casing do not matter.

    +

    @Localizer["Scan the QR Code or enter this key"] @Model.SharedKey @Localizer["into your two factor authenticator app. Spaces and casing do not matter."]

  3. - Once you have scanned the QR code or input the key above, your two factor authentication app will provide you - with a unique code. Enter the code in the confirmation box below. + @Localizer["Once you have scanned the QR code or input the key above, your two factor authentication app will provide you with a unique code. Enter the code in the confirmation box below."]

    - +
    - +
    diff --git a/Server/Areas/Identity/Pages/Account/Manage/Index.cshtml b/Server/Areas/Identity/Pages/Account/Manage/Index.cshtml index e01843779..53bc74f86 100644 --- a/Server/Areas/Identity/Pages/Account/Manage/Index.cshtml +++ b/Server/Areas/Identity/Pages/Account/Manage/Index.cshtml @@ -1,7 +1,9 @@ @page @model IndexModel +@using Microsoft.Extensions.Localization +@inject IStringLocalizer Localizer @{ - ViewData["Title"] = "Profile"; + ViewData["Title"] = Localizer["Profile"]; ViewData["ActivePage"] = ManageNavPages.Index; } @@ -12,15 +14,15 @@
    - +
    - +
    - +
diff --git a/Server/Areas/Identity/Pages/Account/Manage/_ManageNav.cshtml b/Server/Areas/Identity/Pages/Account/Manage/_ManageNav.cshtml index ed5cc11ce..5ca61bf15 100644 --- a/Server/Areas/Identity/Pages/Account/Manage/_ManageNav.cshtml +++ b/Server/Areas/Identity/Pages/Account/Manage/_ManageNav.cshtml @@ -1,15 +1,17 @@ @inject SignInManager SignInManager +@using Microsoft.Extensions.Localization +@inject IStringLocalizer Localizer @{ var hasExternalLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync()).Any(); } diff --git a/Server/Areas/Identity/Pages/Account/Register.cshtml b/Server/Areas/Identity/Pages/Account/Register.cshtml index 0e0a959c2..cc4dd2124 100644 --- a/Server/Areas/Identity/Pages/Account/Register.cshtml +++ b/Server/Areas/Identity/Pages/Account/Register.cshtml @@ -1,8 +1,10 @@ @page @model RegisterModel @inject Remotely.Server.Services.IApplicationConfig AppConfig +@using Microsoft.Extensions.Localization +@inject IStringLocalizer Localizer @{ - ViewData["Title"] = "Register"; + ViewData["Title"] =Localizer["Register"]; }

@ViewData["Title"]

@@ -12,32 +14,32 @@ {
-

Create a new account.

+

@Localizer["Create a new account."]


- +
- +
- +
- +
} else {
-
Registration is disabled.
+
@Localizer["Registration is disabled."]
} diff --git a/Server/Areas/Identity/Pages/Account/Register.cshtml.cs b/Server/Areas/Identity/Pages/Account/Register.cshtml.cs index d8301993b..b401c739f 100644 --- a/Server/Areas/Identity/Pages/Account/Register.cshtml.cs +++ b/Server/Areas/Identity/Pages/Account/Register.cshtml.cs @@ -54,7 +54,7 @@ public RegisterModel( public class InputModel { [Required] - [EmailAddress] + [EmailAddress(ErrorMessage = "The true field is not a valid e-mail address.")] [Display(Name = "Email")] public string Email { get; set; } diff --git a/Server/Areas/Identity/Pages/Shared/_LoginPartial.cshtml b/Server/Areas/Identity/Pages/Shared/_LoginPartial.cshtml index 28975f28b..ce1147863 100644 --- a/Server/Areas/Identity/Pages/Shared/_LoginPartial.cshtml +++ b/Server/Areas/Identity/Pages/Shared/_LoginPartial.cshtml @@ -4,16 +4,17 @@ @inject Remotely.Server.Services.IApplicationConfig AppConfig @inject Remotely.Server.Services.IDataService DataService @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers - +@using Microsoft.Extensions.Localization +@inject IStringLocalizer Localizer diff --git a/Server/Components/AlertsFrame.razor b/Server/Components/AlertsFrame.razor index adb2c96f3..3ea440214 100644 --- a/Server/Components/AlertsFrame.razor +++ b/Server/Components/AlertsFrame.razor @@ -2,7 +2,8 @@ @attribute [Authorize] @inject IDataService DataService @inject IModalService ModalService - +@using Microsoft.Extensions.Localization +@inject IStringLocalizer Localizer } @@ -44,7 +45,7 @@ class="btn btn-sm btn-secondary alert-dismiss-button" type="button" @onclick="()=> { _ = ClearAlert(alert); }"> - Dismiss + @Localizer["Dismiss"] @@ -93,7 +94,7 @@ private void ShowAlertDetails(Alert alert) { - ModalService.ShowModal($"Alert Details for {alert.Device?.DeviceName}", alert.Details.Split('\n')); + ModalService.ShowModal(string.Format( Localizer["Alert Details for {0}"],alert.Device?.DeviceName), alert.Details.Split('\n')); } private void ToggleOpen() diff --git a/Server/Components/ColorPicker.razor b/Server/Components/ColorPicker.razor index 40c3615ee..f8d541dc7 100644 --- a/Server/Components/ColorPicker.razor +++ b/Server/Components/ColorPicker.razor @@ -1,7 +1,8 @@ @using Remotely.Server.Models - + @using Microsoft.Extensions.Localization +@inject IStringLocalizer Localizer
-
Color Sample:
+
@Localizer["Color Sample"]:
Red
diff --git a/Server/Components/Devices/DeviceCard.razor b/Server/Components/Devices/DeviceCard.razor index 5255134b3..b2d98c5bd 100644 --- a/Server/Components/Devices/DeviceCard.razor +++ b/Server/Components/Devices/DeviceCard.razor @@ -1,6 +1,7 @@ @attribute [Authorize] @inherits AuthComponentBase - +@using Microsoft.Extensions.Localization +@inject IStringLocalizer Localizer
-
@@ -34,14 +35,14 @@
@Device.Alias
- +
@if (IsExpanded) { - } @@ -61,10 +62,10 @@ { } - } + }
- +
@Device.CurrentUser @@ -75,25 +76,25 @@ - Actions + @Localizer["Actions"]
  • @@ -101,14 +102,14 @@ OnChanged="OnFileInputChanged"> - Upload File + @Localizer["Upload File"]
  • @@ -116,17 +117,17 @@
    - +
    @Device.Platform
    - + @@ -136,7 +137,7 @@
    - Total Storage + @Localizer["Total Storage"]
    @(Device.TotalStorage) GB @@ -144,14 +145,14 @@
    - +
    @(MathHelper.GetFormattedPercent(Device.UsedMemoryPercent))
    - Total Memory + @Localizer["Total Memory"]
    @(Device.TotalMemory) GB @@ -173,28 +174,28 @@ @if (IsExpanded) {
    - Device ID + @Localizer["Device ID"]
    - Agent Version + @Localizer["Agent Version"]
    - Last Online + @Localizer["Last Online"]
    - Public IP + @Localizer["Public IP"]
    @@ -204,7 +205,7 @@
    - Device Alias + @Localizer["Device Alias"]
    @@ -212,57 +213,57 @@
    - WebRTC Setting + @Localizer["WebRTC Setting"]
    @foreach (var setting in Enum.GetValues(typeof(WebRtcSetting))) { - + } - - -
    + + +
    -
    - Device Group -
    -
    - - +
    + @Localizer["Device Group"] +
    +
    + + - @foreach (var group in DataService.GetDeviceGroups(Username)) + @foreach (var group in DataService.GetDeviceGroups(Username)) { - + } - - -
    +
    + +
    -
    - Tags -
    -
    - - -
    +
    + @Localizer["Tags"] +
    +
    + + +
    -
    - Notes -
    -
    - - -
    +
    + @Localizer["Notes"] +
    +
    + + +
    -
    - -
    +
    + +
    -
    - -
    - +
    + +
    + }
    diff --git a/Server/Components/Devices/DeviceCard.razor.cs b/Server/Components/Devices/DeviceCard.razor.cs index 28f4c6e8c..748c1a182 100644 --- a/Server/Components/Devices/DeviceCard.razor.cs +++ b/Server/Components/Devices/DeviceCard.razor.cs @@ -88,7 +88,7 @@ private void AppState_PropertyChanged(object sender, System.ComponentModel.Prope private void CircuitConnection_MessageReceived(object sender, CircuitEvent e) { - switch (e.EventName) + switch (e.EventName) { case CircuitEventName.DeviceUpdate: case CircuitEventName.DeviceWentOffline: @@ -177,14 +177,14 @@ private async Task HandleValidSubmit() Device.Notes, Device.WebRtcSetting); - ToastService.ShowToast("Device settings saved."); + ToastService.ShowToast(Localizer["Device settings saved."]); await CircuitConnection.TriggerHeartbeat(Device.ID); } private async Task OnFileInputChanged(InputFileChangeEventArgs args) { - ToastService.ShowToast("File upload started."); + ToastService.ShowToast(Localizer["File upload started."]); var fileId = await DataService.AddSharedFile(args.File, User.OrganizationID, OnFileInputProgress); @@ -194,11 +194,11 @@ private async Task OnFileInputChanged(InputFileChangeEventArgs args) if (!result) { - ToastService.ShowToast("Device not found.", classString: "bg-warning"); + ToastService.ShowToast(Localizer["Device not found."], classString: "bg-warning"); } else { - ToastService.ShowToast("File upload completed."); + ToastService.ShowToast(Localizer["File upload completed."]); } } @@ -234,7 +234,7 @@ void modalBody(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder build builder.AddMarkupContent(0, $"
    {disksString}
    "); } - ModalService.ShowModal($"All Disks for {Device.DeviceName}", modalBody); + ModalService.ShowModal($"{Localizer["All Disks for"]} {Device.DeviceName}", modalBody); } private void StartChat() @@ -284,7 +284,7 @@ private void ToggleIsSelected(ChangeEventArgs args) private async Task UninstallAgent() { - var result = await JsInterop.Confirm("Are you sure you want to uninstall this agent? This is permanent!"); + var result = await JsInterop.Confirm(Localizer["Are you sure you want to uninstall this agent? This is permanent!"]); if (result) { await CircuitConnection.UninstallAgents(new[] { Device.ID }); diff --git a/Server/Components/Devices/DevicesFrame.razor b/Server/Components/Devices/DevicesFrame.razor index 002304ed0..7439dcedf 100644 --- a/Server/Components/Devices/DevicesFrame.razor +++ b/Server/Components/Devices/DevicesFrame.razor @@ -1,16 +1,17 @@ @attribute [Authorize] @inherits AuthComponentBase - -

    Devices

    +@using Microsoft.Extensions.Localization +@inject IStringLocalizer Localizer +

    @Localizer["Devices"]

    -
    Device Group
    +
    @Localizer["Device Group"]
    @foreach (var prop in _sortableProperties) @@ -34,21 +35,21 @@
    - +
    - - + +
    -
    Search
    +
    @Localizer["Search"]
    @@ -56,9 +57,9 @@ - Page + @Localizer["Page"] - of + @Localizer["of"] @TotalPages
    diff --git a/Server/Components/Devices/DevicesFrame.razor.cs b/Server/Components/Devices/DevicesFrame.razor.cs index b283389eb..daa56e60b 100644 --- a/Server/Components/Devices/DevicesFrame.razor.cs +++ b/Server/Components/Devices/DevicesFrame.razor.cs @@ -276,7 +276,7 @@ private void FilterDevices() private string GetDisplayName(PropertyInfo propInfo) { - return propInfo.GetCustomAttribute()?.Name ?? propInfo.Name; + return Localizer[propInfo.GetCustomAttribute()?.Name ?? propInfo.Name]; } private string GetSortIcon() @@ -287,7 +287,7 @@ private string GetSortIcon() private void HandleRefreshClicked() { Refresh(); - ToastService.ShowToast("Devices refreshed."); + ToastService.ShowToast(Localizer["Devices refreshed."]); } private void LoadDevices() diff --git a/Server/Components/Devices/Terminal.razor b/Server/Components/Devices/Terminal.razor index 363ca0e4f..d4d8fbe13 100644 --- a/Server/Components/Devices/Terminal.razor +++ b/Server/Components/Devices/Terminal.razor @@ -1,17 +1,18 @@ @inherits AuthComponentBase - +@using Microsoft.Extensions.Localization +@inject IStringLocalizer Localizer
    @@ -49,7 +50,7 @@
    - Editing @EditUser?.UserName + @Localizer["Editing"] @EditUser?.UserName @foreach (var group in DeviceGroups ?? Array.Empty()) @@ -42,7 +43,7 @@ } else { - ToastService.ShowToast("User added to group."); + ToastService.ShowToast(Localizer["User added to group."]); } } @@ -51,11 +52,11 @@ var result = await DataService.RemoveUserFromDeviceGroup(EditUser.OrganizationID, group.ID, EditUser.Id); if (!result) { - ToastService.ShowToast("Failed to remove from group.", classString: "bg-warning"); + ToastService.ShowToast(Localizer["Failed to remove from group."], classString: "bg-warning"); } else { - ToastService.ShowToast("Removed user from group."); + ToastService.ShowToast(Localizer["Removed user from group."]); } } } diff --git a/Server/Components/ModalContents/QuickScriptsSelector.razor b/Server/Components/ModalContents/QuickScriptsSelector.razor index b3141b24b..6814b75ca 100644 --- a/Server/Components/ModalContents/QuickScriptsSelector.razor +++ b/Server/Components/ModalContents/QuickScriptsSelector.razor @@ -1,4 +1,6 @@ - @foreach (var script in QuickScripts) { @@ -6,7 +8,7 @@
    - +
    @code { diff --git a/Server/Components/ModalHarness.razor b/Server/Components/ModalHarness.razor index cb079d2b8..37d68345e 100644 --- a/Server/Components/ModalHarness.razor +++ b/Server/Components/ModalHarness.razor @@ -1,6 +1,7 @@ @inject IModalService ModalService @inject IJsInterop JsInterop - +@using Microsoft.Extensions.Localization +@inject IStringLocalizer Localizer diff --git a/Server/Components/Scripts/RunScript.razor b/Server/Components/Scripts/RunScript.razor index 1cffdfdba..54a56432a 100644 --- a/Server/Components/Scripts/RunScript.razor +++ b/Server/Components/Scripts/RunScript.razor @@ -1,18 +1,19 @@ @inherits AuthComponentBase @attribute [Authorize] - +@using Microsoft.Extensions.Localization +@inject IStringLocalizer Localizer
    - + - If offline, run immediately when next connected + @Localizer["If offline, run immediately when next connected"]
    -
    Saved Scripts
    +
    @Localizer["Saved Scripts"]
    - Show only mine + @Localizer["Show only mine"]
    -
    Devices
    +
    @Localizer["Devices"]
    @foreach (var device in _devices) { diff --git a/Server/Components/Scripts/RunScript.razor.cs b/Server/Components/Scripts/RunScript.razor.cs index 99ab29734..570712620 100644 --- a/Server/Components/Scripts/RunScript.razor.cs +++ b/Server/Components/Scripts/RunScript.razor.cs @@ -97,14 +97,14 @@ private async Task ExecuteScript() { if (_selectedScript is null) { - ToastService.ShowToast("You must select a script.", classString: "bg-warning"); + ToastService.ShowToast(Localizer["You must select a script."], classString: "bg-warning"); return; } if (!_selectedDeviceGroups.Any() && !_selectedDevices.Any()) { - ToastService.ShowToast("You must select at least one device or device group.", classString: "bg-warning"); + ToastService.ShowToast(Localizer["You must select at least one device or device group."], classString: "bg-warning"); return; } @@ -144,11 +144,11 @@ private async Task ExecuteScript() await DataService.AddScriptRun(scriptRun); - ToastService.ShowToast($"Created script run for {scriptRun.Devices.Count} devices."); + ToastService.ShowToast(string.Format( Localizer["Created script run for {0} devices."], scriptRun.Devices.Count)); await CircuitConnection.RunScript(onlineDevices, _selectedScript.Id, scriptRun.Id, ScriptInputType.OneTimeScript, false); - ToastService.ShowToast($"Running script immediately on {onlineDevices.Count()} devices."); + ToastService.ShowToast(string.Format("Running script immediately on {0} devices."), onlineDevices.Count()); } private async Task ScriptSelected(ScriptTreeNode viewModel) diff --git a/Server/Components/Scripts/SavedScripts.razor b/Server/Components/Scripts/SavedScripts.razor index ea70493e8..67f7320f6 100644 --- a/Server/Components/Scripts/SavedScripts.razor +++ b/Server/Components/Scripts/SavedScripts.razor @@ -1,6 +1,7 @@ @inherits AuthComponentBase @attribute [Authorize] - +@using Microsoft.Extensions.Localization +@inject IStringLocalizer Localizer
    @@ -17,7 +18,7 @@
    - + @foreach (var shell in Enum.GetValues()) { @@ -28,32 +29,32 @@
    - +
    - +
    - +
    - +
    - +
    @@ -61,24 +62,24 @@
    @@ -90,12 +91,12 @@
    - +

    - to + @Localizer["to"]
    - +
    @@ -113,15 +114,15 @@
    -
    Environment Variables
    +
    @Localizer["Environment Variables"]
    -
    Device ID
    +
    @Localizer["Device ID"]
    The device ID (GUID) of the computer currently running the script.
    -
    Server URL
    +
    @Localizer["Server URL"]
    The URL of the server that the device connects to (e.g. https://app.remotely.one).
    @@ -136,17 +137,17 @@ class="h-100 w-100" spellcheck="false" autocomplete="off" - placeholder="Enter script text here" /> + placeholder="@Localizer["Enter script text here"]" />
    -
    Saved Scripts
    +
    @Localizer["Saved Scripts"]
    - Show only mine + @Localizer["Show only mine"]
    Add or Edit Schedule @@ -19,16 +20,16 @@
    @@ -38,14 +39,14 @@
    - +
    - +
    @@ -53,7 +54,7 @@
    - + @foreach (var interval in Enum.GetValues()) { @@ -66,10 +67,10 @@
    - +
    - Show only mine + @Localizer["Show only mine"]
    - +
    @foreach (var deviceGroup in _deviceGroups) { @@ -99,7 +100,7 @@
    - +
    @foreach (var device in _devices) { @@ -118,16 +119,16 @@ -

    Saved Schedules

    +

    @Localizer["Saved Schedules"]

    - - - - - - + + + + + + diff --git a/Server/Localization/AcceptLanguageCultureProvider.cs b/Server/Localization/AcceptLanguageCultureProvider.cs new file mode 100644 index 000000000..b7de90779 --- /dev/null +++ b/Server/Localization/AcceptLanguageCultureProvider.cs @@ -0,0 +1,57 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Localization; +using Microsoft.Extensions.Primitives; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; + +namespace Remotely.Server.Localization +{ + public class AcceptLanguageCultureProvider : RequestCultureProvider + { + public AcceptLanguageCultureProvider() + { + SupperCultureInfos = new[] { new CultureInfo("en-US"),new CultureInfo("zh-CN") }; + + } + + public static CultureInfo[] SupperCultureInfos { get; private set; } + private void FillAllCulture(List cultures, CultureInfo cultureInfo) + { + if (!string.IsNullOrEmpty(cultureInfo.Name)) + { + cultures.Add(cultureInfo.Name); + } + if (cultureInfo.Parent != null && !string.IsNullOrEmpty(cultureInfo.Parent.Name)) + { + FillAllCulture(cultures, cultureInfo.Parent); + } + } + + public override Task DetermineProviderCultureResult(HttpContext httpContext) + { + var cultureKey = httpContext.Request.Headers["Accept-Language"]; + try + { + var firstculture = cultureKey.ToString().Split(',')[0]; + var cultures = new List(); + FillAllCulture(cultures, new CultureInfo(firstculture)); + if (!string.IsNullOrEmpty(cultureKey)) + { + var culture = SupperCultureInfos.Where(culture => cultures.Contains(culture.Name)).ToList(); + if (culture.Count > 0) + { + return Task.FromResult(new ProviderCultureResult(culture.Select(d => new StringSegment(d.Name)).ToList())); + } + } + } + catch + { + } + return Task.FromResult(new ProviderCultureResult(SupperCultureInfos[0].Name)); + } + + } +} diff --git a/Server/Localization/JsonLocalizationFactory.cs b/Server/Localization/JsonLocalizationFactory.cs new file mode 100644 index 000000000..c0661a670 --- /dev/null +++ b/Server/Localization/JsonLocalizationFactory.cs @@ -0,0 +1,27 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Options; +using System; +using System.IO; +using System.Reflection; + +namespace Remotely.Server.Localization +{ + public class JsonStringLocalizerFactory : IStringLocalizerFactory + { + private readonly IDistributedCache _cache; + + public JsonStringLocalizerFactory(IDistributedCache cache) + { + _cache = cache; + + } + + public IStringLocalizer Create(Type resourceSource) => + new JsonStringLocalizer(_cache); + + public IStringLocalizer Create(string baseName, string location) => + new JsonStringLocalizer(_cache); + } +} diff --git a/Server/Localization/JsonStringLocalizer.cs b/Server/Localization/JsonStringLocalizer.cs new file mode 100644 index 000000000..d10df599f --- /dev/null +++ b/Server/Localization/JsonStringLocalizer.cs @@ -0,0 +1,122 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Threading; + +namespace Remotely.Server.Localization +{ + public class JsonStringLocalizer : JsonStringLocalizer, IStringLocalizer + { + public JsonStringLocalizer(IDistributedCache cache) : base(cache) + { + + + } + } + public class JsonStringLocalizer : IStringLocalizer + { + + private readonly IDistributedCache _cache; + + private readonly JsonSerializer _serializer = new JsonSerializer(); + + + public JsonStringLocalizer(IDistributedCache cache) + { + _cache = cache; + + } + + public LocalizedString this[string name] + { + get + { + string value = GetString(CultureInfo.CurrentCulture, name); + return new LocalizedString(name, value ?? name, value == null); + } + } + + public LocalizedString this[string name, params object[] arguments] + { + get + { + var actualValue = this[name]; + return !actualValue.ResourceNotFound + ? new LocalizedString(name, string.Format(actualValue.Value, arguments), false) + : actualValue; + } + } + + public IEnumerable GetAllStrings(bool includeParentCultures) + { + string filePath = $"Resources/{Thread.CurrentThread.CurrentCulture.Name}.json"; + using (var str = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read)) + using (var sReader = new StreamReader(str)) + using (var reader = new JsonTextReader(sReader)) + { + while (reader.Read()) + { + if (reader.TokenType != JsonToken.PropertyName) + continue; + string key = (string)reader.Value; + reader.Read(); + string value = _serializer.Deserialize(reader); + yield return new LocalizedString(key, value, false); + } + } + } + + private string GetString(CultureInfo culture, string key) + { + + + string cacheKey = $"locale_{culture.Name}_{key}"; + string cacheValue = _cache.GetString(cacheKey); + if (!string.IsNullOrEmpty(cacheValue)) return cacheValue; + string relativeFilePath = $"Resources/{culture.Name}.json"; + string fullFilePath = Path.GetFullPath(relativeFilePath); + if (File.Exists(fullFilePath)) + { + + string result = GetValueFromJSON(key, Path.GetFullPath(relativeFilePath)); + if (!string.IsNullOrEmpty(result)) _cache.SetString(cacheKey, result); + return result; + } + else + { + if (!string.IsNullOrEmpty(culture.Parent.Name)) + { + return GetString(culture.Parent, key); + } + } + + return default(string); + } + + private string GetValueFromJSON(string propertyName, string filePath) + { + if (propertyName == null) return default; + if (filePath == null) return default; + using (var str = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read)) + using (var sReader = new StreamReader(str)) + using (var reader = new JsonTextReader(sReader)) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.PropertyName && (string)reader.Value == propertyName) + { + reader.Read(); + return _serializer.Deserialize(reader); + } + } + + return default; + } + } + } +} diff --git a/Server/Pages/About.razor b/Server/Pages/About.razor index 7df1b604b..f6d66d41a 100644 --- a/Server/Pages/About.razor +++ b/Server/Pages/About.razor @@ -1,18 +1,19 @@ @page "/about" - -

    About Remotely

    +@using Microsoft.Extensions.Localization +@inject IStringLocalizer Localizer +

    @Localizer["About"] Remotely

    - Website: https://remotely.one + @Localizer["Website"]: https://remotely.one

    - Contact: https://remotely.one/Contact + @Localizer["Contact"]: https://remotely.one/Contact

    - Open-Source Licenses: Credits + @Localizer["Open-Source Licenses"]: Credits

    - Version: @if (System.IO.File.Exists("Remotely_Server.dll")) + @Localizer["Version"]: @if (System.IO.File.Exists("Remotely_Server.dll")) { @System.Diagnostics.FileVersionInfo.GetVersionInfo("Remotely_Server.dll").FileVersion.ToString() } diff --git a/Server/Pages/ApiKeys.razor b/Server/Pages/ApiKeys.razor index a3d1c0754..10f7d8f1d 100644 --- a/Server/Pages/ApiKeys.razor +++ b/Server/Pages/ApiKeys.razor @@ -6,9 +6,10 @@ @inject AuthenticationStateProvider AuthProvider @inject IJsInterop JsInterop @inject IModalService ModalService +@using Microsoft.Extensions.Localization +@inject IStringLocalizer Localizer - -

    API Keys

    +

    @Localizer["API Keys"]

    @if (!string.IsNullOrWhiteSpace(_alertMessage)) { @@ -19,7 +20,7 @@ { if (!string.IsNullOrWhiteSpace(_newKeySecret)) { -
    Warning: The key's secret will only be shown once. Save it now.
    +
    @Localizer["ApiKeyWarn"]
    @@ -28,12 +29,12 @@
    - + + class="form-control form-control-sm custom-control-inline mr-1" + style="width:200px" /> - +
    Script NameRepeat IntervalNext RunLast RunCreated AtCreated By @Localizer["Script Name"] @Localizer["Repeat Interval"] @Localizer["Next Run"] @Localizer["Last Run"] @Localizer["Created At"] @Localizer["Created By"]
    - - - + + + @@ -58,10 +59,10 @@ } @@ -70,7 +71,7 @@ } else { -
    Only organization administrators can view this page.
    +
    @Localizer["AdminViewOnly"]
    } @@ -93,18 +94,18 @@ else await DataService.CreateApiToken(Username, _createKeyName, secretHash); RefreshData(); - _alertMessage = "Key created."; + _alertMessage = Localizer["Key created"]; _newKeySecret = secret; } private async Task DeleteKey(string keyId) { - var result = await JsInterop.Confirm("Are you sure you want to delete this key?"); + var result = await JsInterop.Confirm(Localizer["Are you sure you want to delete this key"]); if (result) { await DataService.DeleteApiToken(Username, keyId); RefreshData(); - _alertMessage = "Key deleted."; + _alertMessage = Localizer["Key deleted"]; } } @@ -120,21 +121,23 @@ else private async Task RenameKey(string keyId) { - var newName = await JsInterop.Prompt("New key name"); + var newName = await JsInterop.Prompt(Localizer["New key name"]); if (!string.IsNullOrWhiteSpace(newName)) { await DataService.RenameApiToken(Username, keyId, newName); RefreshData(); - _alertMessage = "Key renamed."; + _alertMessage = Localizer["Key renamed"]; } } private void ShowApiKeyHelp() { - ModalService.ShowModal("Using API Keys", new[] - { - "API keys should be added to the request header when making API calls. The key should be \"Authorization\", and value should be \"{key-id}:{key-secret}\". Note the colon in between.", - "Example: Authorization=e5da1c09-e851-4bd4-a8c1-532144b3f894:7uY6h5zBYm4+90pZVek4lD6ewbQ83nKcDpghBfG00hhZu6Ew" + ModalService.ShowModal(Localizer["Using API Keys"], new[] + { + Localizer["ApiKeyShowMsg"].Value, + Localizer["ApiKeyExample"].Value + // "API keys should be added to the request header when making API calls. The key should be \"Authorization\", and value should be \"{key-id}:{key-secret}\". Note the colon in between.", + // "Example: Authorization=e5da1c09-e851-4bd4-a8c1-532144b3f894:7uY6h5zBYm4+90pZVek4lD6ewbQ83nKcDpghBfG00hhZu6Ew" }); } } diff --git a/Server/Pages/Branding.razor b/Server/Pages/Branding.razor index 688392db5..47371a0d1 100644 --- a/Server/Pages/Branding.razor +++ b/Server/Pages/Branding.razor @@ -5,13 +5,14 @@ @inject IJsInterop JsInterop @using Remotely.Server.Models @using System.ComponentModel.DataAnnotations - +@using Microsoft.Extensions.Localization +@inject IStringLocalizer Localizer

    Branding

    @if (User?.IsAdministrator != true) { -
    Only organization administrators can view this page.
    +
    @Localizer["AdminViewOnly"]
    } else { @@ -20,7 +21,7 @@ else
    - +
    @@ -33,7 +34,7 @@ else
    - +
    @@ -41,31 +42,31 @@ else @if (!string.IsNullOrWhiteSpace(_base64Icon)) {
    - +
    }
    - +
    - +
    - +
    - +
    - - + +
    @@ -113,8 +114,8 @@ else _base64Icon = Convert.ToBase64String(_inputModel.IconBytes); } - _alertMessage = "Branding saved."; - ToastService.ShowToast("Branding saved."); + _alertMessage = Localizer["Branding saved"]; + ToastService.ShowToast(Localizer["Branding saved"]); } private async Task IconUploadInputChanged(InputFileChangeEventArgs args) @@ -126,7 +127,7 @@ else if (args.File.Size > 1_024_000) { - ToastService.ShowToast("File size must be under 1 MB.", classString: "bg-warning"); + ToastService.ShowToast(Localizer["File size must be under 1 MB"], classString: "bg-warning"); return; } @@ -164,12 +165,12 @@ else private async Task ResetBranding() { - var result = await JsInterop.Confirm("Are you sure you want to reset branding to default?"); + var result = await JsInterop.Confirm(Localizer["ResetBrandConfirm"]); if (result) { await DataService.ResetBranding(User.OrganizationID); await LoadBrandingInfo(); - ToastService.ShowToast("Branding reset."); + ToastService.ShowToast(Localizer["Branding reset"]); } } } diff --git a/Server/Pages/Credits.razor b/Server/Pages/Credits.razor index 9e298e4f9..55d0de50d 100644 --- a/Server/Pages/Credits.razor +++ b/Server/Pages/Credits.razor @@ -1,92 +1,93 @@ @page "/credits" - -

    Credits and Open-Source Licenses

    +@using Microsoft.Extensions.Localization +@inject IStringLocalizer Localizer +

    @Localizer["Credits and Open-Source Licenses"]

    - The below open-source projects are used in Remotely. + @Localizer["The below open-source projects are used in Remotely"]
    - If you've contributed to any of these projects, thank you! + @Localizer["If you've contributed to any of these projects, thank you!"]
    diff --git a/Server/Pages/DeviceDetails.razor b/Server/Pages/DeviceDetails.razor index 9e294cc55..25995a3cc 100644 --- a/Server/Pages/DeviceDetails.razor +++ b/Server/Pages/DeviceDetails.razor @@ -2,28 +2,29 @@ @attribute [Authorize] @inherits AuthComponentBase - +@using Microsoft.Extensions.Localization +@inject IStringLocalizer Localizer @if (string.IsNullOrWhiteSpace(DeviceId)) {
    -

    Device ID:

    +

    @Localizer["Device ID"]:

    -
    - +
    - You can also go directly to a device's details by: + @Localizer["You can also go directly to a device's details by"]:
      -
    • Right-clicking a device card on the main page while it's collapsed
    • -
    • Clicking the "Open in New Tab" button in a device card's header while it's expanded
    • +
    • @Localizer["Right-clicking a device card on the main page while it's collapsed"]
    • +
    • @Localizer["Clicking the \"Open in New Tab\" button in a device card's header while it's expanded"]
    @@ -31,24 +32,24 @@ } else if (Device is null) { -

    Device not found.

    +

    @Localizer["Device not found"]

    } else if (!DataService.DoesUserHaveAccessToDevice(Device.ID, User)) { -

    Unauthorized.

    +

    @Localizer["Unauthorized"]

    } else { - Details + @Localizer["Details"] - Remote Logs + @Localizer["Remote Logs"] - Script History + @Localizer["Script History"] @@ -56,7 +57,7 @@ else

    - Device Details + @Localizer["Device Details"]

    @@ -70,40 +71,40 @@ else
    - +
    - +
    - +
    - +
    - +
    @@ -112,7 +113,7 @@ else
    @@ -120,7 +121,7 @@ else
    @@ -128,21 +129,21 @@ else
    - +
    - +
    @foreach (var setting in Enum.GetValues(typeof(WebRtcSetting))) @@ -155,10 +156,10 @@ else
    - +
    - + @foreach (var group in DataService.GetDeviceGroups(Username)) { @@ -169,21 +170,21 @@ else
    - +
    - +
    - +
    @@ -193,13 +194,13 @@ else
    @if (!Device.IsOnline) { -
    Device must be online to retrieve logs.
    +
    @Localizer["Device must be online to retrieve logs"]
    } else {
    - - + +
    @if (_logLines.Any()) @@ -224,18 +225,18 @@ else

    - Script History + @Localizer["Script History"]"

    NameIDLast Used@Localizer["Name"]@Localizer["ID"]@Localizer["Last Used"]
    @apiToken.ID @apiToken.LastUsed - + - +
    - - - - - - - + + + + + + + diff --git a/Server/Pages/Downloads.razor b/Server/Pages/Downloads.razor index 6e7efb13e..63d537963 100644 --- a/Server/Pages/Downloads.razor +++ b/Server/Pages/Downloads.razor @@ -1,18 +1,19 @@ @page "/downloads" @using Microsoft.AspNetCore.Hosting -@using Microsoft.Extensions.Logging +@using Microsoft.Extensions.Logging @inject AuthenticationStateProvider AuthProvider @inject IDataService DataService @inject UserManager UserManager @inject IWebHostEnvironment HostEnv @inject NavigationManager NavManager -@inject ILogger Logger - +@inject ILogger Logger +@using Microsoft.Extensions.Localization +@inject IStringLocalizer Localizer
    -

    Portable Instant Support Clients

    +

    @Localizer["Portable Instant Support Clients"]

    - Instant desktop sharing. No account required. + @Localizer["Instant desktop sharing. No account required."]
    Windows (64-Bit) @@ -35,26 +36,26 @@ @*
    macOS x64 (10.12 - 10.15)

    - macOS x64 Executable + macOS x64 Executable

    macOS arm64 (11.01)

    - macOS arm64 Executable + macOS arm64 Executable

    -
    *@ +
    *@
    -

    Installable Instant Support Clients

    +

    @Localizer["Installable Instant Support Clients"]

    - Light-weight, self-updating quick support clients. + @Localizer["Light-weight, self-updating quick support clients."]
    @if (!_isServerUrlEmbedded) {
    - +
    } @@ -62,7 +63,7 @@
    Windows (64-Bit)

    - Note: Only the default organization's branding will apply to these. + @Localizer["Note: Only the default organization's branding will apply to these."]

    Windows Installer @@ -71,7 +72,7 @@

    Windows (32-Bit)

    - Note: Only the default organization's branding will apply to these. + @Localizer["Note: Only the default organization's branding will apply to these."]

    Windows Installer @@ -81,15 +82,15 @@

    -

    Resident Agents

    +

    @Localizer["Resident Agents"]

    - Installable background agents that provide unattended access and remote scripting. + @Localizer["Installable background agents that provide unattended access and remote scripting."]
    @if (!_isAuthenticated) {
    -
    Must be logged in to download.
    +
    @Localizer["Must be logged in to download."]
    } @@ -98,7 +99,7 @@
    Windows 10 / 8.1 / 7 (64-Bit and 32-Bit)

    - Note: GPU-accelerated screen capture and PowerShell Core is unavailable on Windows 7. + @Localizer["Note: GPU-accelerated screen capture and PowerShell Core is unavailable on Windows 7."]

    Windows Installer (x64/x86) @@ -109,8 +110,8 @@

    -

    Example Quiet Install:
    - +
    @Localizer["Example Quiet Install"]:
    + Remotely_Installer.exe -install @@ -120,13 +121,13 @@

    -

    Example Quiet Uninstall:
    +
    @Localizer["Example Quiet Uninstall"]:
    Remotely_Installer.exe -uninstall -quiet

    -

    Example Local Install:
    - +
    @Localizer["Example Local Install"]:
    + Remotely_Installer.exe -install @@ -137,8 +138,8 @@

    -

    All Override Options:
    - +
    @Localizer["All Override Options"]:
    + Remotely_Installer.exe -install -quiet -supportshortcut -organizationid "0b3d706b-9c5d-41e6-8ae9-5720d16324e6" @@ -160,17 +161,17 @@ Linux x64 Files Only

    -

    Example Install:
    - +
    @Localizer["Example Install"]:
    + sudo [path]/Install-Ubuntu-x64.sh

    -

    Example Local Install:
    - +
    @Localizer["Example Local Install"]:
    + sudo [path]/Install-Ubuntu-x64.sh --path [path]/Remotely-Linux.zip

    -

    Uninstall:
    +
    @Localizer["Uninstall"]:
    sudo [path]/Install-Ubuntu-x64.sh --uninstall

    @@ -185,25 +186,25 @@

    @*macOS arm64 (11.01) -

    +

    macOS arm64 Bash Installer
    macOS arm64 Files Only -

    *@ +

    *@

    -

    Example Install:
    - +
    @Localizer["Example Install"]:
    + sudo [path]/Install-MacOS-x64.sh

    -

    Example Local Install:
    - +
    @Localizer["Example Local Install"]:
    + sudo [path]/Install-MacOS-x64.sh --path [path]/Remotely-MacOS-x64.zip

    -

    Example Uninstall:
    - +
    @Localizer["Example Uninstall"]:
    + sudo [path]/Install-MacOS-x64.sh --uninstall

    @@ -248,7 +249,7 @@ } catch (Exception ex) { - Logger.LogWarning(ex, "Error while checking ClickOnce file."); + Logger.LogWarning(ex, @Localizer["Error while checking ClickOnce file."]); } finally { diff --git a/Server/Pages/Error.cshtml b/Server/Pages/Error.cshtml index 5a8125947..32d8b79ad 100644 --- a/Server/Pages/Error.cshtml +++ b/Server/Pages/Error.cshtml @@ -1,6 +1,7 @@ @page @model Remotely.Server.Pages.ErrorModel - +@using Microsoft.Extensions.Localization +@inject IStringLocalizer Localizer @@ -15,26 +16,18 @@
    -

    Error.

    -

    An error occurred while processing your request.

    +

    @Localizer["Error"]

    +

    @Localizer["An error occurred while processing your request."]

    @if (Model.ShowRequestId) {

    - Request ID: @Model.RequestId + @Localizer["Request ID"]: @Model.RequestId

    } -

    Development Mode

    -

    - Swapping to the Development environment displays detailed information about the error that occurred. -

    -

    - The Development environment shouldn't be enabled for deployed applications. - It can result in displaying sensitive information from exceptions to end users. - For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development - and restarting the app. -

    +

    @Localizer["Development Mode"]

    + @Html.Raw(@Localizer["DevelopmentDesc"])
    diff --git a/Server/Pages/GetSupport.cshtml b/Server/Pages/GetSupport.cshtml index 41d4c2a08..bc40f09d2 100644 --- a/Server/Pages/GetSupport.cshtml +++ b/Server/Pages/GetSupport.cshtml @@ -1,14 +1,16 @@ @page @model Remotely.Server.Pages.GetSupportModel +@using Microsoft.Extensions.Localization +@inject IStringLocalizer Localizer @{ - ViewData["Title"] = "Get Support"; + ViewData["Title"] = Localizer["Get Support"]; } @if (!Request.Query.ContainsKey("deviceID")) { -

    Get Support

    +

    @Localizer["Get Support"]

    - Device ID is missing. Please use a valid shortcut to the support page, which will include the device ID. + @Localizer["Device ID is missing. Please use a valid shortcut to the support page, which will include the device ID."]

    } else @@ -21,35 +23,35 @@ else @Model.StatusMessage
    } -

    Get Support

    +

    @Localizer["Get Support"]

    - +
    - +
    - +
    - +
    - +
    @@ -62,7 +64,7 @@ else @section Scripts { - + } } diff --git a/Server/Pages/Index.razor b/Server/Pages/Index.razor index fbba21121..10e305d2b 100644 --- a/Server/Pages/Index.razor +++ b/Server/Pages/Index.razor @@ -1,21 +1,23 @@ @page "/" @inject IApplicationConfig AppConfig @inject IDataService DataService - +@using Microsoft.Extensions.Localization +@inject IStringLocalizer Localizer

    Remotely

    - An open-source remote support solution. + @Localizer["An open-source remote support solution"] +


    - Log In + @Localizer["Log In"] @if (AppConfig.MaxOrganizationCount < 0 || DataService.GetOrganizationCount() < AppConfig.MaxOrganizationCount) { - Register + @Localizer["Register"] }

    diff --git a/Server/Pages/Invite.cshtml b/Server/Pages/Invite.cshtml index 002979e14..f8f78966e 100644 --- a/Server/Pages/Invite.cshtml +++ b/Server/Pages/Invite.cshtml @@ -1,9 +1,11 @@ @page @model Remotely.Server.Pages.InviteModel +@using Microsoft.Extensions.Localization +@inject IStringLocalizer Localizer @{ - ViewData["Title"] = "Invite"; + ViewData["Title"] =Localizer["Invite"]; } -

    Organization Invite

    +

    @Localizer["Organization Invite"]


    @@ -11,7 +13,7 @@ {
    Congratulations!

    - You've successfully joined the organization! + @Localizer["You've successfully joined the organization!"]

    } @@ -19,15 +21,15 @@ {
    - WARNING: -

    You will leave your current organization and lose access to its agents unless someone is able to invite you back in.

    -

    Are you sure you want to leave your current organization and join this one?

    + @Localizer["WARNING"]: +

    @Localizer["You will leave your current organization and lose access to its agents unless someone is able to invite you back in."]

    +

    @Localizer["Are you sure you want to leave your current organization and join this one?"]

    - - + +
    } diff --git a/Server/Pages/ManageOrganization.razor b/Server/Pages/ManageOrganization.razor index 95a37f3db..73027033c 100644 --- a/Server/Pages/ManageOrganization.razor +++ b/Server/Pages/ManageOrganization.razor @@ -1,8 +1,9 @@ @page "/manage-organization" @attribute [Authorize] @inherits AuthComponentBase - -

    Manage Organization

    +@using Microsoft.Extensions.Localization +@inject IStringLocalizer Localizer +

    @Localizer["Manage Organization"]

    @if (User?.IsAdministrator == true) @@ -11,11 +12,11 @@
    @* Organization ID *@
    - +
    - + @@ -30,24 +31,24 @@
    @* Organization Name *@
    - +
    - * Requires browser refresh. + * @Localizer["Requires browser refresh."]
    @if (User.IsServerAdmin) {
    - + @@ -62,7 +63,7 @@
    @* Device Groups *@
    - + @@ -75,15 +76,15 @@
    - +
    - - +
    @@ -94,18 +95,18 @@
    @* Users *@
    - +
    ShellTimestampUserDurationInputOutputError@Localizer["Shell"]@Localizer["Timestamp"]@Localizer["User"]@Localizer["Duration"]@Localizer["Input"]@Localizer["Output"]@Localizer["Error"]
    - - - - - + + + + + @@ -116,16 +117,16 @@ @if (User.Id == orgUser.Id) { - + - + } else { - - - + + + } } @@ -140,17 +141,17 @@
    - +
    User NameAdministratorDevice GroupsReset PasswordDelete User@Localizer["User Name"]@Localizer["Administrator"]@Localizer["Device Groups"]@Localizer["Reset Password"]@Localizer["Delete User"]
    - - - - + + + + @@ -161,10 +162,10 @@ - + } @@ -179,16 +180,16 @@
    - +
    - Admin? + @Localizer["Admin?"]
    @@ -196,7 +197,7 @@
    - +
    @@ -206,6 +207,6 @@ } else { -
    Only organization administrators can view this page.
    +
    @Localizer["Only organization administrators can view this page."]
    } diff --git a/Server/Pages/ManageOrganization.razor.cs b/Server/Pages/ManageOrganization.razor.cs index 7db02067b..542441628 100644 --- a/Server/Pages/ManageOrganization.razor.cs +++ b/Server/Pages/ManageOrganization.razor.cs @@ -81,7 +81,7 @@ private void CreateNewDeviceGroup() return; } - ToastService.ShowToast("Device group created."); + ToastService.ShowToast(Localizer["Device group created."]); _deviceGroups.Add(deviceGroup); _newDeviceGroupName = string.Empty; } @@ -95,7 +95,7 @@ private void DefaultOrgCheckChanged(ChangeEventArgs args) var isDefault = (bool)args.Value; DataService.SetIsDefaultOrganization(_organization.ID, isDefault); - ToastService.ShowToast("Default organization set."); + ToastService.ShowToast(Localizer["Default organization set."]); } private async Task DeleteInvite(InviteLink invite) @@ -105,7 +105,7 @@ private async Task DeleteInvite(InviteLink invite) return; } - var result = await JsInterop.Confirm("Are you sure you want to delete this invitation?"); + var result = await JsInterop.Confirm(Localizer["Are you sure you want to delete this invitation?"]); if (!result) { return; @@ -113,7 +113,7 @@ private async Task DeleteInvite(InviteLink invite) DataService.DeleteInvite(User.OrganizationID, invite.ID); _invites.RemoveAll(x => x.ID == invite.ID); - ToastService.ShowToast("Invitation deleted."); + ToastService.ShowToast(Localizer["Invitation deleted."]); } private async Task DeleteSelectedDeviceGroup() @@ -128,7 +128,7 @@ private async Task DeleteSelectedDeviceGroup() return; } - var result = await JsInterop.Confirm("Are you sure you want to delete this device group?"); + var result = await JsInterop.Confirm(Localizer["Are you sure you want to delete this device group?"]); if (!result) { return; @@ -148,11 +148,11 @@ private async Task DeleteUser(RemotelyUser user) if (User.Id == user.Id) { - ToastService.ShowToast("You can't delete yourself.", classString: "bg-warning"); + ToastService.ShowToast(Localizer["You can't delete yourself."], classString: "bg-warning"); return; } - var result = await JsInterop.Confirm("Are you sure you want to delete this user?"); + var result = await JsInterop.Confirm(Localizer["Are you sure you want to delete this user?"]); if (!result) { return; @@ -160,7 +160,7 @@ private async Task DeleteUser(RemotelyUser user) await DataService.DeleteUser(User.OrganizationID, user.Id); _orgUsers.RemoveAll(x => x.Id == user.Id); - ToastService.ShowToast("User deleted."); + ToastService.ShowToast(Localizer["User deleted."]); } private async Task EditDeviceGroups(RemotelyUser user) @@ -174,7 +174,7 @@ void editDeviceGroupsModal(RenderTreeBuilder builder) builder.AddAttribute(2, EditDeviceGroup.DeviceGroupsPropName, deviceGroups); builder.CloseComponent(); } - await ModalService.ShowModal("Device Groups", editDeviceGroupsModal); + await ModalService.ShowModal(Localizer["Device Groups"], editDeviceGroupsModal); } private async Task EvaluateInviteInputKeypress(KeyboardEventArgs args) @@ -207,14 +207,14 @@ private void OrganizationNameChanged(ChangeEventArgs args) if (newName.Length > 25) { - ToastService.ShowToast("Must be 25 characters or less.", + ToastService.ShowToast(Localizer["Must be 25 characters or less."], classString: "bg-warning"); return; } DataService.UpdateOrganizationName(_organization.ID, newName); _organization.OrganizationName = newName; - ToastService.ShowToast("Organization name changed."); + ToastService.ShowToast(Localizer["Organization name changed."]); } private async Task RefreshData() @@ -241,11 +241,11 @@ private async Task ResetPassword(RemotelyUser user) code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); var resetUrl = $"{NavManager.BaseUri}Identity/Account/ResetPassword?code={code}"; - await ModalService.ShowModal("Password Reset", builder => + await ModalService.ShowModal(Localizer["Password Reset"], builder => { - builder.AddMarkupContent(0, $@"
    Password Reset URL:
    + builder.AddMarkupContent(0, $@"
    {Localizer["Password Reset URL:"]}
    -
    NOTE: Give this URL to the user. They must be logged out completely for it to work.
    "); +
    {Localizer["NOTE: Give this URL to the user. They must be logged out completely for it to work."]}
    "); }); } @@ -269,12 +269,12 @@ private async Task SendInvite() _inviteAsAdmin = false; _inviteEmail = string.Empty; - ToastService.ShowToast("User account created."); + ToastService.ShowToast(Localizer["User account created."]); return; } else { - ToastService.ShowToast("Create user failed.", classString: "bg-danger"); + ToastService.ShowToast(Localizer["Create user failed."], classString: "bg-danger"); return; } } @@ -300,7 +300,7 @@ private async Task SendInvite() User.OrganizationID); if (emailResult) { - ToastService.ShowToast("Invitation sent."); + ToastService.ShowToast(Localizer["Invitation sent."]); _inviteAsAdmin = false; _inviteEmail = string.Empty; @@ -308,7 +308,7 @@ private async Task SendInvite() } else { - ToastService.ShowToast("Error sending invititation email.", classString: "bg-danger"); + ToastService.ShowToast(Localizer["Error sending invititation email."], classString: "bg-danger"); } } } @@ -322,58 +322,50 @@ private void SetUserIsAdmin(ChangeEventArgs args, RemotelyUser orgUser) var isAdmin = (bool)args.Value; DataService.ChangeUserIsAdmin(User.OrganizationID, orgUser.Id, isAdmin); - ToastService.ShowToast("Administrator value set."); + ToastService.ShowToast(Localizer["Administrator value set."]); } private void ShowDefaultOrgHelp() { - ModalService.ShowModal("Default Organization", new[] + ModalService.ShowModal(Localizer["Default Organization"], new[] { - @"This option is only available for server administrators. When selected, - it sets this organization as the default for the server. If the organization can't - be determined in the quick support apps, they will use the default organization's branding." + Localizer["ShowDefaultOrgHelp"].Value + // Localizer[@"This option is only available for server administrators. When selected, it sets this organization as the default for the server. If the organization can't be determined in the quick support apps, they will use the default organization's branding."].Value }); } private void ShowDeviceGroupHelp() { - ModalService.ShowModal("Device Groups", new[] + ModalService.ShowModal(Localizer["Device Groups"], new[] { - "Device groups can be used to restrict user permissions and to filter computers on " + - "the main page.", - "Everyone will have access to devices that are not in a group. Only " + - "administrators and users in a device group will have access to devices in that group." + Localizer["DeviceGroupHelp"].Value + //Localizer["Device groups can be used to restrict user permissions and to filter computers on the main page. Everyone will have access to devices that are not in a group. Only administrators and users in a device group will have access to devices in that group."].Value }); } private void ShowInvitesHelp() { - ModalService.ShowModal("Invitations", new[] + ModalService.ShowModal(Localizer["Invitations"], new[] { - "All pending invitations will be shown here and can be revoked by deleting them.", - - "If a user does not exist, sending an invite will create their account and add them to the current organization. " + - "A password reset URL can be generated from the user table.", - - "The Admin checkbox determines if the new user will have administrator privileges in this organization." + Localizer["ShowInvitesHelp"].Value + // "All pending invitations will be shown here and can be revoked by deleting them. If a user does not exist, sending an invite will create their account and add them to the current organization.A password reset URL can be generated from the user table.The Admin checkbox determines if the new user will have administrator privileges in this organization." }); } private void ShowRelayCodeHelp() { - ModalService.ShowModal("Relay Code", new[] + ModalService.ShowModal(Localizer["Relay Code"], new[] { - @"This relay code will be appended to EXE filenames. If the clients were built - from source and have the server URL embedded, they will use this code to look - up your organization's branding to use." + Localizer["ShowRelayCodeHelp"].Value + //@"This relay code will be appended to EXE filenames. If the clients were built from source and have the server URL embedded, they will use this code to look up your organization's branding to use." }); } private void ShowUsersHelp() { - ModalService.ShowModal("Users", new[] + ModalService.ShowModal(Localizer["Users"], new[] { - "All users for the organization are managed here", - "Administrators will have access to this management screen as well as all computers." + Localizer["ShowUsersHelp"].Value + // "All users for the organization are managed here Administrators will have access to this management screen as well as all computers." }); } } diff --git a/Server/Pages/RemoteControl.cshtml b/Server/Pages/RemoteControl.cshtml index 5a34834f7..d5b689ba8 100644 --- a/Server/Pages/RemoteControl.cshtml +++ b/Server/Pages/RemoteControl.cshtml @@ -2,6 +2,8 @@ @using Remotely.Shared.Models @inject Remotely.Server.Services.IApplicationConfig AppConfig @model Remotely.Server.Pages.RemoteControlModel +@using Microsoft.Extensions.Localization +@inject IStringLocalizer Localizer @{ Layout = null; } @@ -13,7 +15,7 @@ - Remotely Remote Control + @Localizer["Remotely Remote Control"] @@ -72,12 +74,12 @@
    - Remotely + @Localizer["Remotely"]
    -
    Support Portal
    +
    @Localizer["Support Portal"]
    - @@ -89,84 +91,84 @@
    - +
    @@ -226,7 +228,7 @@
    @@ -249,7 +251,7 @@ diff --git a/Server/Pages/ScriptsPage.razor b/Server/Pages/ScriptsPage.razor index 59cc67ac8..68af499ae 100644 --- a/Server/Pages/ScriptsPage.razor +++ b/Server/Pages/ScriptsPage.razor @@ -2,18 +2,19 @@ @inherits AuthComponentBase @using System.Collections @inject IDataService DataService - +@using Microsoft.Extensions.Localization +@inject IStringLocalizer Localizer - Saved Scripts + @Localizer["Saved Scripts"] - Run Script + @Localizer["Run Script"] - Script Schedules + @Localizer["Script Schedules"] diff --git a/Server/Pages/ServerConfig.razor b/Server/Pages/ServerConfig.razor index ccedb988c..909caa09b 100644 --- a/Server/Pages/ServerConfig.razor +++ b/Server/Pages/ServerConfig.razor @@ -2,25 +2,26 @@ @attribute [Authorize] @inherits AuthComponentBase - +@using Microsoft.Extensions.Localization +@inject IStringLocalizer Localizer @if (User?.IsServerAdmin == true) { -

    Server Info

    +

    @Localizer["Server Info"]

    - +
    @AgentHub.ServiceConnections.Count
    - +
    - +
    @TotalDevices
    - +
    @CircuitManager.Connections.Count
    @@ -48,19 +49,19 @@
    -

    Server Admins

    +

    @Localizer["Server Admins"]

    - +
    - Show my organization only + @Localizer["Show my organization only"]
    - Show current admins only + @Localizer["Show current admins only"]

    @@ -82,20 +83,20 @@ -

    Application Settings

    +

    @Localizer["Application Settings"]

    - +

    - +
    - +
    - - +
    - +

    - +
    @foreach (var proxy in Input.KnownProxies) @@ -160,7 +161,7 @@ }
    - +
    - +
    - +

    - +

    - +

    - +

    - +

    - +

    - +

    - +

    - +

    - +

    - +

    - +

    - +
    - This sometimes needs to be disabled for Let's Encrypt certificates. + @Localizer["This sometimes needs to be disabled for Let's Encrypt certificates."]

    @@ -269,31 +270,31 @@
    - +

    - +

    - +

    - +
    - +
    @foreach (var origin in Input.TrustedCorsOrigins) @@ -316,22 +317,22 @@
    - +
    - - +
    - +

    @@ -339,7 +340,7 @@
    - +

    @@ -347,15 +348,15 @@
    - +
    - Must be edited in appsettings.json. + @Localizer["Must be edited in appsettings.json."]
    -

    Connection Strings

    +

    @Localizer["Connection Strings"]

    - +

    @@ -363,7 +364,7 @@
    - +

    @@ -372,7 +373,7 @@
    - +

    @@ -381,7 +382,7 @@
    - +
    @@ -390,5 +391,5 @@ } else { -
    Only organization administrators can view this page.
    +
    @Localizer["Only organization administrators can view this page."]
    } \ No newline at end of file diff --git a/Server/Pages/ServerConfig.razor.cs b/Server/Pages/ServerConfig.razor.cs index a7083f3b9..e76f3e106 100644 --- a/Server/Pages/ServerConfig.razor.cs +++ b/Server/Pages/ServerConfig.razor.cs @@ -76,7 +76,7 @@ public class AppSettingsModel public string SmtpDisplayName { get; set; } [Display(Name = "SMTP Email")] - [EmailAddress] + [EmailAddress(ErrorMessage = "The true field is not a valid e-mail address.")] public string SmtpEmail { get; set; } [Display(Name = "SMTP Host")] @@ -377,12 +377,12 @@ private void ShowOutdatedDevices() .GetDevices(OutdatedDevices) .Select(x => x.DeviceName); - ModalService.ShowModal("Outdated Devices", + ModalService.ShowModal(Localizer["Outdated Devices"], (new[] { "Outdated Devices:" }).Concat(outdatedDeviceNames).ToArray()); } else { - ModalService.ShowModal("Outdated Devices", new[] { "There are no outdated devices currently online." }); + ModalService.ShowModal(Localizer["Outdated Devices"], new[] { Localizer["There are no outdated devices currently online."].Value }); } } diff --git a/Server/Pages/ServerLogs.razor b/Server/Pages/ServerLogs.razor index 58ea34a15..3f34dea09 100644 --- a/Server/Pages/ServerLogs.razor +++ b/Server/Pages/ServerLogs.razor @@ -5,8 +5,9 @@ @inject IDataService DataService @inject IToastService ToastService @inject IJsInterop JsInterop - -

    Server Logs

    +@using Microsoft.Extensions.Localization +@inject IStringLocalizer Localizer +

    @Localizer["Server Logs"]

    @if (User?.IsAdministrator == true) @@ -14,28 +15,28 @@
    - Type: + @Localizer["Type"]:
    - Filter: + @Localizer["Filter"]:
    - From: + @Localizer["From"]:
    - To: + @Localizer["To"]:
    @@ -62,11 +63,11 @@
    Invited UserAdministratorLink(s)Delete@Localizer["Invited User"]@Localizer["Administrator"]@Localizer["Link(s)"]@Localizer["Delete"]
    - - - - - + + + + + @@ -85,7 +86,7 @@ } else { -
    Only organization administrators can view this page.
    +
    @Localizer["Only organization administrators can view this page."]
    } @code { @@ -111,11 +112,11 @@ else private async Task ClearAllLogs() { - var result = await JsInterop.Confirm("Are you sure you want to delete all logs?"); + var result = await JsInterop.Confirm(Localizer["Are you sure you want to delete all logs?"]); if (result) { await DataService.ClearLogs(User.UserName); - ToastService.ShowToast("Logs deleted."); + ToastService.ShowToast(Localizer["Logs deleted."]); } } diff --git a/Server/Pages/UserOptions.razor b/Server/Pages/UserOptions.razor index 19e1b8930..b8ae33fda 100644 --- a/Server/Pages/UserOptions.razor +++ b/Server/Pages/UserOptions.razor @@ -7,8 +7,9 @@ @inject IDataService DataService @inject IToastService ToastService @inject IModalService ModalService - -

    User Options

    +@using Microsoft.Extensions.Localization +@inject IStringLocalizer Localizer +

    @Localizer["User Options"]

    @@ -23,15 +24,15 @@ }
    - +
    - +
    - +
    @foreach (var setting in Enum.GetValues(typeof(Theme))) @@ -39,13 +40,13 @@ } -
    * Requires browser refresh.
    +
    * @Localizer["Requires browser refresh."]
    - +
    @@ -121,9 +122,9 @@ DataService.UpdateUserOptions(User.UserName, _options); - _alertMessage = "Options saved"; + _alertMessage = Localizer["Options saved"]; - ToastService.ShowToast("Options saved."); + ToastService.ShowToast(Localizer["Options saved."]); return Task.CompletedTask; } @@ -135,10 +136,10 @@ private void ShowShortcutHelp() { - var modalText = @"The shell shortcuts are used to quickly switch between terminal shells on the main page. - If you type one of these shortcuts into the terminal, it will select the corresponding command - mode (e.g. PowerShell Core, Bash, etc.)."; + //var modalText = @"The shell shortcuts are used to quickly switch between terminal shells on the main page. + //If you type one of these shortcuts into the terminal, it will select the corresponding command + //mode (e.g. PowerShell Core, Bash, etc.)."; - ModalService.ShowModal("Shell Shortcuts", new[] { modalText }); + ModalService.ShowModal("Shell Shortcuts", new[] { Localizer["modalText"].Value }); } } diff --git a/Server/Pages/_Host.cshtml b/Server/Pages/_Host.cshtml index df5db9313..1d043b909 100644 --- a/Server/Pages/_Host.cshtml +++ b/Server/Pages/_Host.cshtml @@ -1,11 +1,12 @@ @page "/" +@using Microsoft.Extensions.Localization @using Remotely.Server.Services @using Remotely.Shared.Models @namespace Remotely.Server.Pages @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers @inject IDataService DataService @inject IApplicationConfig AppConfig - +@inject IStringLocalizer Localizer @{ Layout = null; var user = DataService.GetUserByNameWithOrg(User?.Identity?.Name); @@ -65,7 +66,7 @@ An unhandled exception has occurred. See browser dev tools for details. - Reload + @Localizer["Reload"] 🗙
    diff --git a/Server/Resources/en-US.json b/Server/Resources/en-US.json new file mode 100644 index 000000000..bfb47345b --- /dev/null +++ b/Server/Resources/en-US.json @@ -0,0 +1,407 @@ +{ + "Remotely": "Remotely", + "Reload": "Reload", + "An open-source remote support solution": "An open-source remote support solution.", + "About": "About", + "Website": "Website", + "Contact": "Contact", + "Open-Source Licenses": "Open-Source Licenses", + "Version": "Version", + "API Keys": "API Keys", + "New Token Name": "New Token Name", + "ApiKeyWarn": "Warning: The key's secret will only be shown once. Save it now.", + "Create": "Create", + "Name": "Name", + "ID": "ID", + "Last Used": "Last Used", + "Rename": "Rename", + "Delete": "Delete", + "AdminViewOnly": "Only organization administrators can view this page.", + "Key created": "Key created", + "Key deleted": "Key deleted", + "New key name": "New key name", + "Key renamed": "Key renamed", + "Using API Keys": "Using API Keys", + "ApiKeyShowMsg": "API keys should be added to the request header when making API calls. The key should be \"Authorization\", and value should be \"{key-id}:{key-secret}\". Note the colon in between.", + "ApiKeyExample": "Example: Authorization=e5da1c09-e851-4bd4-a8c1-532144b3f894:7uY6h5zBYm4+90pZVek4lD6ewbQ83nKcDpghBfG00hhZu6Ew", + "Branding Areas": "Branding Areas", + "Product Name": "Product Name", + "Current Icon": "Current Icon", + "New Icon": "New Icon", + "Title Foreground": "Title Foreground", + "Title Background": "Title Background", + "Button Color": "Button Color", + "Reset": "Reset", + "Submit": "Submit", + "Branding saved": "Branding saved.", + "File size must be under 1 MB": "File size must be under 1 MB", + "ResetBrandConfirm": "Are you sure you want to reset branding to default?", + "Branding reset": "Branding reset", + "Credits and Open-Source Licenses": "Credits and Open-Source Licenses", + "The below open-source projects are used in Remotely": "The below open-source projects are used in Remotely.", + "If you've contributed to any of these projects, thank you!": "If you've contributed to any of these projects, thank you!", + "License": "License", + "Project": "Project", + "Device ID": "Device ID", + "Enter a device ID to see its details": "Enter a device ID to see its details.", + "Go": "Go", + "You can also go directly to a device's details by": "You can also go directly to a device's details by.", + "Right-clicking a device card on the main page while it's collapsed": "Right-clicking a device card on the main page while it's collapsed", + "Clicking the \"Open in New Tab\" button in a device card's header while it's expanded": "Clicking the \"Open in New Tab\" button in a device card's header while it's expanded", + "Device not found": "Device not found", + "Unauthorized": "Unauthorized", + "Details": "Details", + "Remote Logs": "Remote Logs", + "Script History": "Script History", + "Device Details": "Device Details", + "Device": "Device", + "Agent Version": "Agent Version", + "Platform": "Platform", + "Disk": "Disk", + "View All": "View All", + "Total Storage": "Total Storage", + "Memory": "Memory", + "Total Memory": "Total Memory", + "Device Alias": "Device Alias", + "WebRTC Setting": "WebRTC Setting", + "Device Group": "Device Group", + "None": "None", + "Tags": "Tags", + "Notes": "Notes", + "Save": "Save", + "Device must be online to retrieve logs": "Device must be online to retrieve logs", + "Refresh": "Refresh", + "Delete Logs": "Delete Logs", + "Shell": "Shell", + "Timestamp": "Timestamp", + "User": "User", + "Duration": "Duration", + "Input": "Input", + "Output": "Output", + "Error": "Error", + "Portable Instant Support Clients": "Portable Instant Support Clients", + "Instant desktop sharing. No account required.": "Instant desktop sharing. No account required.", + "Installable Instant Support Clients": "Installable Instant Support Clients", + "Light-weight, self-updating quick support clients.": "Light-weight, self-updating quick support clients.", + "Must be built from source to target specific server URL.": "Must be built from source to target specific server URL.", + "Note: Only the default organization's branding will apply to these.": "Note: Only the default organization's branding will apply to these.", + "Resident Agents": "Resident Agents", + "Installable background agents that provide unattended access and remote scripting.": "Installable background agents that provide unattended access and remote scripting.", + "Must be logged in to download.": "Must be logged in to download.", + "Note: GPU-accelerated screen capture and PowerShell Core is unavailable on Windows 7.": "Note: GPU-accelerated screen capture and PowerShell Core is unavailable on Windows 7.", + "Example Quiet Install": "Example Quiet Install", + "Example Quiet Uninstall": "Example Quiet Uninstall", + "Example Local Install": "Example Local Install", + "All Override Options": "All Override Options", + "Example Install": "Example Install", + "Uninstall": "Uninstall", + "Error while checking ClickOnce file.": "Error while checking ClickOnce file.", + "An error occurred while processing your request.": "An error occurred while processing your request.", + "Request ID": "Request ID", + "Development Mode": "Development Mode", + "DevelopmentDesc": "

    Swapping to theDevelopment environment displays detailed information about the error that occurred.

    The Development environment shouldn't be enabled for deployed applications.It can result in displaying sensitive information from exceptions to end users.For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Developmentand restarting the app.

    ", + "Get Support": "Get Support", + "Device ID is missing. Please use a valid shortcut to the support page, which will include the device ID.": "Device ID is missing. Please use a valid shortcut to the support page, which will include the device ID.", + "Your Name": "Your Name", + "Email": "Email", + "Phone": "Phone", + "Chat Response OK?": "Chat Response OK?", + "Log In": "Log In", + "Register": "Register", + "Invite": "Invite", + "Organization Invite": "Organization Invite", + "You've successfully joined the organization!": "You've successfully joined the organization!", + "WARNING": "WARNING", + "You will leave your current organization and lose access to its agents unless someone is able to invite you back in.": "You will leave your current organization and lose access to its agents unless someone is able to invite you back in.", + "Are you sure you want to leave your current organization and join this one?": "Are you sure you want to leave your current organization and join this one?", + "Cancel": "Cancel", + "Confirm": "Confirm", + "Manage Organization": "Manage Organization", + "Organization ID": "Organization ID", + "Relay Code": "Relay Code", + "Organization Name": "Organization Name", + "Requires browser refresh.": "Requires browser refresh.", + "Default Organization For Server": "Default Organization For Server", + "Device Groups": "Device Groups", + "Add new device group": "Add new device group", + "Add": "Add", + "Users": "Users", + "User Name": "User Name", + "Administrator": "Administrator", + "Reset Password": "Reset Password", + "Delete Group": "Delete Group", + "Delete User": "Delete User", + "Edit": "Edit", + "Invitations": "Invitations", + "Invited User": "Invited User", + "Link(s)": "Link(s)", + "Join Link": "Join Link", + "Add User": "Add User", + "Email of user to add": "Email of user to add", + "Admin?": "Admin?", + "Only organization administrators can view this page.": "Only organization administrators can view this page.", + "Device group created.": "Device group created.", + "Default organization set.": "Default organization set.", + "Are you sure you want to delete this invitation?": "Are you sure you want to delete this invitation?", + "Invitation deleted.": "Invitation deleted.", + "Are you sure you want to delete this device group?": "Are you sure you want to delete this device group?", + "You can't delete yourself.": "You can't delete yourself.", + "Are you sure you want to delete this user?": "Are you sure you want to delete this user?", + "User deleted.": "User deleted.", + "Must be 25 characters or less.": "Must be 25 characters or less.", + "Organization name changed.": "Organization name changed.", + "Password Reset": "Password Reset", + "NOTE: Give this URL to the user. They must be logged out completely for it to work.": "NOTE: Give this URL to the user. They must be logged out completely for it to work.", + "User account created.": "User account created.", + "Create user failed.": "Create user failed.", + "Invitation sent.": "Invitation sent.", + "Error sending invititation email.": "Error sending invititation email.", + "Administrator value set.": "Administrator value set.", + "Default Organization": "Default Organization", + "ShowDefaultOrgHelp": "This option is only available for server administrators. When selected, it sets this organization as the default for the server. If the organization can't be determined in the quick support apps, they will use the default organization's branding.", + "DeviceGroupHelp": "Device groups can be used to restrict user permissions and to filter computers on the main page. Everyone will have access to devices that are not in a group. Only administrators and users in a device group will have access to devices in that group.", + "ShowInvitesHelp": "All pending invitations will be shown here and can be revoked by deleting them. If a user does not exist, sending an invite will create their account and add them to the current organization.A password reset URL can be generated from the user table.The Admin checkbox determines if the new user will have administrator privileges in this organization.", + "ShowRelayCodeHelp": "This relay code will be appended to EXE filenames. If the clients were built from source and have the server URL embedded, they will use this code to look up your organization's branding to use.", + "ShowUsersHelp": "All users for the organization are managed here Administrators will have access to this management screen as well as all computers.", + "Remotely Remote Control": "Remotely Remote Control", + "Support Portal": "Support Portal", + "keyboardButtontitle": "Invoke the mobile touch keyboard.", + "Actions": "Actions", + "View Only": "View Only", + "View Only title": "If toggled, prevents sending commnads and input to the remote computer.", + "Clipboard": "Clipboard", + "Clipboardtitle": "Type the current clipboard text on the remote computer.", + "Block Remote Input": "Block Remote Input", + "Block Remote Inputtitle": "Prevent remote user from using keyboard and mouse.", + "Invite Others": "Invite Others", + "Invite Otherstitle": "Copy a link that lets another person view the remote screen.", + "Audio": "Audio", + "Audiotitle": "Windows only. Stream the remote audio to the browser.", + "File Transfer": "File Transfer", + "File Transfertitle": "Transfer files to the remote computer.", + "ctrlAltDelButtontitle": "Simulate the Ctrl+Alt+Del command on the remote computer.", + "Disconnect": "Disconnect", + "Disconnecttitle": "Disconnect from the current session.", + "View": "View", + "Stream Mode": "Stream Mode", + "Stream Modetitle": "Reduce bandwidth and increase FPS, but increase input delay.", + "Monitors": "Monitors", + "Monitorstitle": "Switch monitors on remote multi-monitor setups.", + "Fullscreen": "Fullscreen", + "Fullscreentitle": "Enter fullscreen mode.", + "Fit": "Fit", + "Fittitle": "If toggled, will resize image to fit in the window.", + "Recording": "Recording", + "Start": "Start", + "Starttitle": "Record session as a WEBM video in the browser.", + "Download": "Download", + "Downloadtitle": "Download the recorded session as a WEBM file.", + "Windows Session": "Windows Session", + "Connection": "Connection", + "Connection is relayed": "Connection is relayed", + "Connection is peer-to-peer": "Connection is peer-to-peer", + "Shared Clipboard": "Shared Clipboard", + "Type Clipboard": "Type Clipboard", + "Upload File": "Upload File", + "Download File": "Download File", + "Connect to a client": "Connect to a client", + "Your Name (shown to client)": "Your Name (shown to client)", + "Connect": "Connect", + "Disconnected from client.": "Disconnected from client.", + "Translucency Software": "Translucency Software", + "Saved Scripts": "Saved Scripts", + "Run Script": "Run Script", + "Script Schedules": "Script Schedules", + "Server Info": "Server Info", + "Devices Online": "Devices Online", + "Devices Outdated": "Devices Outdated", + "Devices Total": "Devices Total", + "Active Users": "Active Users", + "Server Admins": "Server Admins", + "Show my organization only": "Show my organization only", + "Show current admins only": "Show current admins only", + "Application Settings": "Application Settings", + "Allow API Login": "Allow API Login", + "Banned Devices": "Banned Devices", + "Remove": "Remove", + "Add banned device ID, name, or IP": "Add banned device ID, name, or IP", + "Data Retention in Days": "Data Retention in Days", + "Database Provider": "Database Provider", + "Enable Windows Event Log": "Enable Windows Event Log", + "Enforce Attended Access": "Enforce Attended Access", + "Known Proxies": "Known Proxies", + "Max Organization Count": "Max Organization Count", + "Max Concurrent Updates": "Max Concurrent Updates", + "Message of the Day": "Message of the Day", + "Redirect to HTTPS": "Redirect to HTTPS", + "Remote Control Notify User": "Remote Control Notify User", + "Require Authentication on Remote Control Page": "Require Authentication on Remote Control Page", + "Remote Control Session Limit": "Remote Control Session Limit", + "Require 2FA": "Require 2FA", + "SMTP Display Name": "SMTP Display Name", + "SMTP Email": "SMTP Email", + "SMTP Host": "SMTP Host", + "SMTP Port": "SMTP Port", + "SMTP Check Certificate Revocation": "SMTP Check Certificate Revocation", + "This sometimes needs to be disabled for Let's Encrypt certificates.": "This sometimes needs to be disabled for Let's Encrypt certificates.", + "SMTP Local Domain": "SMTP Local Domain", + "SMTP Username": "SMTP Username", + "SMTP Password": "SMTP Password", + "Test": "Test", + "Theme": "Theme", + "Trusted CORS Origins": "Trusted CORS Origins", + "Add trusted URL": "Add trusted URL", + "Use HSTS": "Use HSTS", + "Use WebRTC": "Use WebRTC", + "ICE Servers": "ICE Servers", + "Must be edited in appsettings.json.": "Must be edited in appsettings.json.", + "Connection Strings": "Connection Strings", + "PostgreSQL": "PostgreSQL", + "SQLite": "SQLite", + "SQL Server": "SQL Server", + "Server Logs": "Server Logs", + "Download Logs": "Download Logs", + "Download Script History": "Download Script History", + "Type": "Type", + "All": "All", + "Filter": "Filter", + "From": "From", + "To": "To", + "Message": "Message", + "Source": "Source", + "Stack Trace": "Stack Trace", + "Are you sure you want to delete all logs?": "Are you sure you want to delete all logs?", + "Logs deleted.": "Logs deleted.", + "User Options": "User Options", + "Shown to clients instead of your email": "Shown to clients instead of your email", + "Command Shortcuts": "Command Shortcuts", + "Options saved": "Options saved", + "modalText": "The shell shortcuts are used to quickly switch between terminal shells on the main page.If you type one of these shortcuts into the terminal, it will select the corresponding command mode (e.g. PowerShell Core, Bash, etc.).", + "Log out": "Log out", + "Log in": "Log in", + "Two-Factor Authentication Required": "Two-Factor Authentication Required", + "Two-factor authentication is required. Click the button below to set up your authenticator app.": "Two-factor authentication is required. Click the button below to set up your authenticator app.", + "Enable 2FA": "Enable 2FA", + "Home": "Home", + "Remote Control": "Remote Control", + "Downloads": "Downloads", + "Scripts": "Scripts", + "Organization": "Organization", + "Branding": "Branding", + "Server Config": "Server Config", + "Account": "Account", + "Logout": "Logout", + "Login": "Login", + "Create a new account.": "Create a new account.", + "Password": "Password", + "ConfirmPassword": "ConfirmPassword", + "Registration is disabled.": "Registration is disabled.", + "Use a local account to log in.": "Use a local account to log in.", + "RememberMe": "RememberMe", + "Forgot your password?": "Forgot your password?", + "Register as a new user": "Register as a new user", + "Resend email confirmation": "Resend email confirmation", + "Confirm email change": "Confirm email change", + "Confirm email": "Confirm email", + "Profile": "Profile", + "Username": "Username", + "PhoneNumber": "PhoneNumber", + "Configure authenticator app": "Configure authenticator app", + "To use an authenticator app go through the following steps:": "To use an authenticator app go through the following steps:", + "Download a two-factor authenticator app like Microsoft Authenticator for": "Download a two-factor authenticator app like Microsoft Authenticator for", + "Google Authenticator for": "Google Authenticator for", + "Scan the QR Code or enter this key": "Scan the QR Code or enter this key", + "into your two factor authenticator app. Spaces and casing do not matter.": "into your two factor authenticator app. Spaces and casing do not matter.", + "Once you have scanned the QR code or input the key above, your two factor authentication app will provide you with a unique code. Enter the code in the confirmation box below.": "Once you have scanned the QR code or input the key above, your two factor authentication app will provide you with a unique code. Enter the code in the confirmation box below.", + "Verification Code": "Verification Code", + "Verify": "Verify", + "External logins": "External logins", + "Two-factor authentication": "Two-factor authentication", + "Personal data": "Personal data", + "Open in New Tab": "Open in New Tab", + "Chat": "Chat", + "Last Online": "Last Online", + "Public IP": "Public IP", + "Device settings saved.": "Device settings saved.", + "File upload started.": "File upload started.", + "Device not found.": "Device not found.", + "File upload completed.": "File upload completed.", + "All Disks for": "All Disks for", + "Are you sure you want to uninstall this agent? This is permanent!": "Are you sure you want to uninstall this agent? This is permanent!", + "Hide offline devices": "Hide offline devices", + "Select All": "Select All", + "Page": "Page", + "of": "of", + "per page": "per page", + "Devices refreshed.": "Devices refreshed.", + "Terminal": "Terminal", + "Quick Scripts": "Quick Scripts", + "Enter a terminal command": "Enter a terminal command", + "Editing": "Editing", + "User added to group.": "User added to group.", + "Failed to remove from group.": "Failed to remove from group.", + "Removed user from group.": "Removed user from group.", + "Run": "Run", + "You must select a script.": "You must select a script.", + "You must select at least one device or device group.": "You must select at least one device or device group.", + "Created script run for {0} devices.": "Created script run for {0} devices.", + "Running script immediately on {0} devices.": "Running script immediately on {0} devices.", + "Script Name": "Script Name", + "Folder Path": "Folder Path", + "Public?": "Public?", + "Quick Script?": "Quick Script?", + "Creator": "Creator", + "New": "New", + "Alert Options": "Alert Options", + "Environment Variables": "Environment Variables", + "Send Email": "Send Email", + "to": "to", + "Generate Alert": "Generate Alert", + "Server URL": "Server URL", + "Enter script text here": "Enter script text here", + "Show only mine": "Show only mine", + "Schedule Name": "Schedule Name", + "Start At": "Start At", + "Repeat Interval": "Repeat Interval", + "Saved Schedules": "Saved Schedules", + "Next Run": "Next Run", + "Last Run": "Last Run", + "Created At": "Created At", + "Created By": "Created By", + "Clear All": "Clear All", + "View Details": "View Details", + "Dismiss": "Dismiss", + "Alert Details for {0}": "Alert Details for {0}", + "Color Sample": "Color Sample", + "Close": "Close", + "The password and confirmation password do not match.": "The password and confirmation password do not match.", + "The {0} must be at least {2} and at max {1} characters long.": "The {0} must be at least {2} and at max {1} characters long.", + "The true field is not a valid e-mail address.": "The true field is not a valid e-mail address.", + "Search device properties": "Search device properties", + "Sort": "Sort", + "Alias": "Alias", + "CPU Utilization": "CPU Utilization", + "Current User": "Current User", + "Device Name": "Device Name", + "OS Description": "OS Description", + "Processor Count": "Processor Count", + "Memory Total": "Memory Total", + "Storage Total": "Storage Total", + "Memory Used": "Memory Used", + "Memory Used %": "Memory Used %", + "Storage Used": "Storage Used", + "Storage Used %": "Storage Used %", + "Search": "Search", + "Enter a name for your organization": "Enter a name for your organization", + "Enter your email.": "Enter your email.", + "Password Reset URL:": "Password Reset URL:", + "Outdated Devices": "Outdated Devices", + "There are no outdated devices currently online.": "There are no outdated devices currently online.", + "Add a known proxy": "Add a known proxy" + + + + + + +} diff --git a/Server/Resources/zh-CN.json b/Server/Resources/zh-CN.json new file mode 100644 index 000000000..adf807921 --- /dev/null +++ b/Server/Resources/zh-CN.json @@ -0,0 +1,400 @@ +{ + "Reload": "刷新", + "An open-source remote support solution": "一个开源的远程解决方案。", + "About": "关于", + "Website": "网站", + "Contact": "联系", + "Open-Source Licenses": "开源协议", + "Version": "版本", + "API Keys": "API密钥", + "New Token Name": "新的Token名称", + "ApiKeyWarn": "警告,此密钥只显示一次,请妥善保存!", + "Create": "新建", + "Name": "名称", + "ID": "ID", + "Last Used": "最后使用", + "Rename": "重命名", + "Delete": "删除", + "AdminViewOnly": "只有组织管理员才能访问此页面", + "Key created": "密钥已创建", + "Key deleted": "密钥已删除", + "New key name": "新的密钥名称", + "Key renamed": "密钥已重命名", + "Using API Keys": "使用 API 密钥", + "ApiKeyShowMsg": "当进行API调用时,应该将API键添加到请求头。key应该是\"Authorization\", value应该是\"{key-id}:{key-secret}\"。注意中间的冒号。", + "ApiKeyExample": "例如: Authorization=e5da1c09-e851-4bd4-a8c1-532144b3f894:7uY6h5zBYm4+90pZVek4lD6ewbQ83nKcDpghBfG00hhZu6Ew", + "Branding Areas": "品牌区域", + "Product Name": "产品名称", + "Current Icon": "当前图标", + "New Icon": "新图标", + "Title Foreground": "标题前景色", + "Title Background": "标题背景色", + "Button Color": "按钮颜色", + "Reset": "重置", + "Submit": "提交", + "Branding saved": "品牌已保存!", + "File size must be under 1 MB": "文件大小必须小于 1 MB", + "ResetBrandConfirm": "您确定要将品牌重置为默认吗?", + "Branding reset": "品牌已重置", + "Credits and Open-Source Licenses": "许可证和开源许可", + "The below open-source projects are used in Remotely": "以下是Remotely用到的开源项目", + "If you've contributed to any of these projects, thank you!": "如果你对这些项目有贡献,谢谢!", + "License": "许可证", + "Project": "项目", + "Device ID": "设备 ID", + "Enter a device ID to see its details": "输入一个设备 ID 以查看明细", + "Go": "Go", + "You can also go directly to a device's details by": "你也可以通过如下方式查看设备明细.", + "Right-clicking a device card on the main page while it's collapsed": "选中一个在主页没有展开的设备卡片并右键它", + "Clicking the \"Open in New Tab\" button in a device card's header while it's expanded": "点击设备卡片头部的 \"在新的选项卡打开\" 按钮", + "Device not found": "没有找到对应设备", + "Unauthorized": "没有授权", + "Details": "明细", + "Remote Logs": "远程日志", + "Script History": "脚本历史", + "Device Details": "设备明细", + "Device": "设备", + "Agent Version": "代理端版本", + "Platform": "平台", + "Disk": "磁盘", + "View All": "查看所有", + "Total Storage": "存储空间", + "Memory": "内存", + "Total Memory": "总内存", + "Device Alias": "设备别名", + "WebRTC Setting": "WebRTC设置", + "Device Group": "设备组", + "None": "没有", + "Tags": "标签", + "Notes": "注释", + "Save": "保存", + "Device must be online to retrieve logs": "设备必须在线才能接收日志", + "Refresh": "刷新", + "Delete Logs": "删除日志", + "Shell": "Shell", + "Timestamp": "时间戳", + "User": "用户", + "Duration": "持续时间", + "Input": "输入", + "Output": "输出", + "Error": "错误", + "Portable Instant Support Clients": "便携式支持客户端", + "Instant desktop sharing. No account required.": "不需要安装就能使用的客户端", + "Installable Instant Support Clients": "可安装的支持客户端", + "Light-weight, self-updating quick support clients.": "轻量级,支持更新,支持客户端。", + "Must be built from source to target specific server URL.": "URL必须在构建服务的时候指定", + "Note: Only the default organization's branding will apply to these.": "注意:只有默认组织的品牌有效。", + "Resident Agents": "无人值守客户端", + "Installable background agents that provide unattended access and remote scripting.": "可安装的后台代理,提供无人值守访问和远程脚本的功能", + "Must be logged in to download.": "下载前必须登录", + "Note: GPU-accelerated screen capture and PowerShell Core is unavailable on Windows 7.": "注意:gpu加速屏幕截图和PowerShell Core在Windows 7上不可用。", + "Example Quiet Install": "静默安装示例", + "Example Quiet Uninstall": "静默卸载示例", + "Example Local Install": "本地安装示例", + "All Override Options": "覆盖所有选项", + "Example Install": "安装示例", + "Uninstall": "卸载", + "Error while checking ClickOnce file.": "检查ClickOnce文件时出错。", + "An error occurred while processing your request.": "处理请求时发生错误。", + "Request ID": "请求 ID", + "Development Mode": "开发者模式", + "DevelopmentDesc": "

    切换到开发者 环境将会显示异常明细

    不应该为部署的应用程序启用开发环境。它会导致将异常中的敏感信息显示给最终用户。为本地调试,要开启 开发者 环境,只需把ASPNETCORE_ENVIRONMENT 环境变量设置为 Development然后重启此应用

    ", + "Get Support": "获取支持", + "Device ID is missing. Please use a valid shortcut to the support page, which will include the device ID.": "设备ID丢失。请使用有效的链接跳转到支持页面,其中将包括设备ID。", + "Your Name": "你的名字", + "Email": "邮箱", + "Phone": "电话", + "Chat Response OK?": "可以聊天吗?", + "Log In": "登录", + "Register": "注册", + "Invite": "邀请", + "Organization Invite": "组织邀请", + "You've successfully joined the organization!": "你已经成功地加入了这个组织!", + "WARNING": "警告", + "You will leave your current organization and lose access to its agents unless someone is able to invite you back in.": "你将离开你现在的组织,失去与它的代理的联系,除非有人能够邀请你回去。", + "Are you sure you want to leave your current organization and join this one?": "你确定要离开当前的组织加入这个新的组织吗?", + "Cancel": "取消", + "Confirm": "提交", + "Manage Organization": "组织管理", + "Organization ID": "组织 ID", + "Relay Code": "中继代码", + "Organization Name": "组织名称", + "Requires browser refresh.": "请求浏览器刷新", + "Default Organization For Server": "服务器的默认组织", + "Device Groups": "设备组", + "Add new device group": "新建设备组", + "Add": "新增", + "Users": "用户", + "User Name": "用户名称", + "Administrator": "管理员", + "Reset Password": "重置密码", + "Delete Group": "删除组", + "Delete User": "删除用户", + "Edit": "编辑", + "Invitations": "邀请", + "Invited User": "邀请用户", + "Link(s)": "链接", + "Join Link": "加入链接", + "Add User": "增加用户", + "Email of user to add": "用户名或者邮件", + "Admin?": "管理员?", + "Only organization administrators can view this page.": "只有组织管理员可以查看该页面。", + "Device group created.": "设备组创建。", + "Default organization set.": "默认设置的组织。", + "Are you sure you want to delete this invitation?": "您确定要删除此邀请吗?", + "Invitation deleted.": "邀请删除。", + "Are you sure you want to delete this device group?": "确定删除该设备组吗?", + "You can't delete yourself.": "You can't delete yourself.", + "Are you sure you want to delete this user?": "你不能删除自己。", + "User deleted.": "用户删除。", + "Must be 25 characters or less.": "必须少于25个字符。", + "Organization name changed.": "组织名称发生了变化", + "Password Reset": "密码重置", + "NOTE: Give this URL to the user. They must be logged out completely for it to work.": "注意:将此URL提供给用户。它们必须完全登出才能工作。", + "User account created.": "创建用户帐户。", + "Create user failed.": "创建用户失败。", + "Invitation sent.": "邀请发送。", + "Error sending invititation email.": "发送邀请邮件错误。", + "Administrator value set.": "管理员设置值。", + "Default Organization": "默认的组织", + "ShowDefaultOrgHelp": "此选项仅对服务器管理员可用。选中后,它将此组织设置为服务器的默认组织。如果无法在快速支持应用程序中确定组织,它们将使用默认组织的品牌。", + "DeviceGroupHelp": "设备组可用于限制用户权限和在主页上过滤计算机。每个人都可以使用非分组的设备。只有设备组中的管理员和用户才能访问该组中的设备。", + "ShowInvitesHelp": "所有待定的邀请将显示在这里,并可以通过删除它们来撤销。如果用户不存在,发送邀请将创建他们的帐户,并将他们添加到当前组织。可以从用户表中生成密码重置URL。Admin复选框确定新用户在该组织中是否具有管理员特权。", + "ShowRelayCodeHelp": "这个中继代码将被附加到EXE文件名中。如果客户机是从源代码构建的,并且嵌入了服务器URL,那么它们将使用此代码来查找要使用的组织品牌。", + "ShowUsersHelp": "该组织的所有用户都在这里进行管理。管理员将可以访问此管理界面以及所有计算机。", + "Remotely Remote Control": "Remotely 远程控制", + "Support Portal": "支持门户", + "keyboardButtontitle": "调用移动触摸键盘。", + "Actions": "动作", + "View Only": "仅查看", + "View Only title": "如果勾选此选项,将防止向远程计算机发送命令和输入。", + "Clipboard": "剪贴板", + "Clipboardtitle": "在远程计算机上键入当前剪贴板文本。", + "Block Remote Input": "中断远程输入", + "Block Remote Inputtitle": "禁止远程用户使用键盘和鼠标。", + "Invite Others": "邀请其他人", + "Invite Otherstitle": "复制一个链接,让另一个人看到远程屏幕。", + "Audio": "音频", + "Audiotitle": "仅Windows。将远程音频流传输到浏览器。", + "File Transfer": "文件传输", + "File Transfertitle": "在远程计算机之间传输文件", + "ctrlAltDelButtontitle": "在远程计算机上发送Ctrl+Alt+Del命令。", + "Disconnect": "断开连接", + "Disconnecttitle": "断开当前会话。", + "View": "视图", + "Stream Mode": "流模式", + "Stream Modetitle": "降低带宽,增加FPS,但增加输入延迟。", + "Monitors": "显示器", + "Monitorstitle": "在远程计算机的多个显示器之间切换", + "Fullscreen": "全屏", + "Fullscreentitle": "进入全屏模式", + "Fit": "适合屏幕", + "Fittitle": "如果勾选此选项,将调整图像大小以适应窗口。", + "Recording": "录像", + "Start": "开始", + "Starttitle": "在浏览器中以WEBM视频的形式录制会话。", + "Download": "下载", + "Downloadtitle": "将录制的会话下载为WEBM文件。", + "Windows Session": "Windows会话", + "Connection": "连接状态", + "Connection is relayed": "连接流量通过服务器转发", + "Connection is peer-to-peer": "点对点连接", + "Shared Clipboard": "共享剪贴板", + "Type Clipboard": "输入剪切板信息", + "Upload File": "上传文件", + "Download File": "下载文件", + "Connect to a client": "连接到客户端", + "Your Name (shown to client)": "您的姓名(在客户端显示)", + "Connect": "连接", + "Disconnected from client.": "断开客户端。", + "Translucency Software": "Translucency Software", + "Saved Scripts": "保存脚本", + "Run Script": "执行脚本", + "Script Schedules": "脚本时间表", + "Server Info": "服务器信息", + "Devices Online": "在线设备", + "Devices Outdated": "设备已更新", + "Devices Total": "总设备", + "Active Users": "活跃用户", + "Server Admins": "服务器管理员", + "Show my organization only": "只显示我的组织", + "Show current admins only": "仅显示当前管理员", + "Application Settings": "应用程序设置", + "Allow API Login": "允许API登录", + "Banned Devices": "禁止设备", + "Remove": "移除", + "Add banned device ID, name, or IP": "添加禁用的设备ID、名称或IP", + "Data Retention in Days": "以天为单位保存数据", + "Database Provider": "数据库提供者", + "Enable Windows Event Log": "启用Windows事件日志", + "Enforce Attended Access": "强制参加访问", + "Known Proxies": "已知的代理", + "Max Organization Count": "最大组织数", + "Max Concurrent Updates": "最大的并发更新", + "Message of the Day": "每日提示", + "Redirect to HTTPS": "重定向到HTTPS", + "Remote Control Notify User": "远程控制通知用户", + "Require Authentication on Remote Control Page": "需要在远程控制页面上进行身份验证", + "Remote Control Session Limit": "远程控制会话限制", + "Require 2FA": "要求 2FA", + "SMTP Display Name": "SMTP显示名称", + "SMTP Email": "SMTP 邮箱", + "SMTP Host": "SMTP 主机", + "SMTP Port": "SMTP 端口", + "SMTP Check Certificate Revocation": "SMTP检查证书吊销", + "This sometimes needs to be disabled for Let's Encrypt certificates.": "对于Let's Encrypt有时需要禁用此功能。", + "SMTP Local Domain": "SMTP本地域", + "SMTP Username": "SMTP 用户名", + "SMTP Password": "SMTP 密码", + "Test": "测试", + "Theme": "主题", + "Trusted CORS Origins": "信任的 CORS Origins", + "Add trusted URL": "添加信任URL", + "Use HSTS": "使用 HSTS", + "Use WebRTC": "使用 WebRTC", + "ICE Servers": "ICE 服务器", + "Must be edited in appsettings.json.": "必须在appsettings.json中编辑。", + "Connection Strings": "连接字符串", + "PostgreSQL": "PostgreSQL", + "SQLite": "SQLite", + "SQL Server": "SQL Server", + "Server Logs": "服务器日志", + "Download Logs": "下载日志", + "Download Script History": "下载脚本历史", + "Type": "类型", + "All": "所有", + "Filter": "筛选", + "From": "来自", + "To": "目标", + "Message": "消息", + "Source": "源", + "Stack Trace": "堆栈跟踪", + "Are you sure you want to delete all logs?": "您确定要删除所有日志吗?", + "Logs deleted.": "日志删除.", + "User Options": "用户选项", + "Shown to clients instead of your email": "对客户端隐藏你的邮箱", + "Command Shortcuts": "命令快捷键", + "Options saved": "选项保存", + "modalText": "shell快捷方式用于在主页面上的终端shell之间快速切换。如果您在终端中输入这些快捷方式之一,它将选择相应的命令模式(例如PowerShell Core、Bash等)。", + "Log out": "登出", + "Log in": "登入", + "Two-Factor Authentication Required": "要求两步验证", + "Two-factor authentication is required. Click the button below to set up your authenticator app.": "需要双因素身份验证。单击下面的按钮来设置您的验证程序。", + "Enable 2FA": "启用 2FA", + "Home": "主页", + "Remote Control": "远程控制", + "Downloads": "下载", + "Scripts": "脚本", + "Organization": "组织", + "Branding": "品牌", + "Server Config": "服务器配置", + "Account": "账户", + "Logout": "登出", + "Login": "登录", + "Create a new account.": "建立一个新账号。", + "Password": "密码", + "ConfirmPassword": "确认密码", + "Registration is disabled.": "注册已被禁用。", + "Use a local account to log in.": "使用本地帐号登录。", + "RememberMe": "记住我", + "Forgot your password?": "忘记密码?", + "Register as a new user": "注册为一个新用户", + "Resend email confirmation": "重发验证邮件", + "Confirm email change": "确认邮件更改", + "Confirm email": "确认邮件", + "Profile": "配置文件", + "Username": "用户名", + "PhoneNumber": "电话号码", + "Configure authenticator app": "配置身份验证应用程序", + "To use an authenticator app go through the following steps:": "要使用验证器应用程序,请执行以下步骤:", + "Download a two-factor authenticator app like Microsoft Authenticator for": "下载一个双因素验证程序,如Microsoft authenticator", + "Google Authenticator for": "Google Authenticator for", + "Scan the QR Code or enter this key": "扫描二维码或输入此值", + "into your two factor authenticator app. Spaces and casing do not matter.": "在两步验证APP中,忽略空格", + "Once you have scanned the QR code or input the key above, your two factor authentication app will provide you with a unique code. Enter the code in the confirmation box below.": "一旦你扫描了二维码或输入了上面的密钥,你的双因子认证应用程序就会为你提供一个唯一的代码。在下面的确认框中输入代码。", + "Verification Code": "验证码", + "Verify": "验证", + "External logins": "外部登录", + "Two-factor authentication": "双因素身份验证", + "Personal data": "个人资料", + "Open in New Tab": "在新选项卡中打开", + "Chat": "聊天", + "Last Online": "最后一次在线", + "Public IP": "公网 IP", + "Device settings saved.": "设备设置保存。", + "File upload started.": "文件上传开始。", + "Device not found.": "找不到设备", + "File upload completed.": "文件上传完成。", + "All Disks for": "所有的磁盘", + "Are you sure you want to uninstall this agent? This is permanent!": "您确定要卸载此代理吗?这是永久的!", + "Hide offline devices": "隐藏离线设备", + "Select All": "全选", + "Page": "页", + "of": "/", + "per page": "每页数量", + "Devices refreshed.": "设备更新。", + "Terminal": "终端", + "Quick Scripts": "快速脚本", + "Enter a terminal command": "输入终端命令", + "Editing": "修改中", + "User added to group.": "用户已添加到组。", + "Failed to remove from group.": "从组中删除失败。", + "Removed user from group.": "从组中删除用户。", + "Run": "运行", + "You must select a script.": "您必须选择一个脚本。", + "You must select at least one device or device group.": "必须选择至少一个设备或设备组。", + "Created script run for {0} devices.": "为{0}设备创建脚本运行。", + "Running script immediately on {0} devices.": "在{0}设备上立即运行脚本。", + "Script Name": "脚本名称", + "Folder Path": "文件夹的路径", + "Public?": "公开?", + "Quick Script?": "快速脚本?", + "Creator": "创建者", + "New": "新建", + "Alert Options": "警报选项", + "Environment Variables": "环境变量", + "Send Email": "发送电子邮件", + "to": "发送到", + "Generate Alert": "生成警报", + "Server URL": "服务器 URL", + "Enter script text here": "在这里输入脚本命令", + "Show only mine": "只显示我", + "Schedule Name": "计划名", + "Start At": "开始", + "Repeat Interval": "重复间隔", + "Saved Schedules": "保存计划", + "Next Run": "下次运行", + "Last Run": "最后运行", + "Created At": "创建于", + "Created By": "创建者", + "Clear All": "清除所有", + "View Details": "查看详细信息", + "Dismiss": "解散", + "Alert Details for {0}": "警报细节:{0}", + "Color Sample": "颜色样例", + "Close": "关闭", + "The password and confirmation password do not match.": "密码与确认密码不匹配。", + "The {0} must be at least {2} and at max {1} characters long.": "{0}必须至少为{2},最长为{1}。", + "The true field is not a valid e-mail address.": "不是有效的电子邮件地址。", + "Search device properties": "搜索设备属性", + "Sort": "排序", + "Alias": "别名", + "CPU Utilization": "CPU利用率", + "Current User": "当前用户", + "Device Name": "设备名称", + "OS Description": "操作系统描述", + "Processor Count": "处理器数量", + "Memory Total": "总内存", + "Storage Total": "存储总量", + "Memory Used": "内存占用", + "Memory Used %": "内存占用 %", + "Storage Used": "存储占用", + "Storage Used %": "存储占用 %", + "Search": "搜索", + "Enter a name for your organization": "为你的组织取一个名字", + "Enter your email.": "输入你的邮箱。", + "Password Reset URL:": "密码重置链接:", + "Outdated Devices": "过时的设备", + "There are no outdated devices currently online.": "目前线上还没有过时的设备。", + "Add a known proxy": "添加一个已知的代理" +} diff --git a/Server/Shared/LoginDisplay.razor b/Server/Shared/LoginDisplay.razor index 690445964..74618f642 100644 --- a/Server/Shared/LoginDisplay.razor +++ b/Server/Shared/LoginDisplay.razor @@ -1,12 +1,14 @@ - +@using Microsoft.Extensions.Localization +@inject IStringLocalizer Localizer + Account
    - +
    - Register - Log in + @Localizer["Register"] + @Localizer["Log in"]
    diff --git a/Server/Shared/MainLayout.razor b/Server/Shared/MainLayout.razor index 9f69fb50f..aabea2a0d 100644 --- a/Server/Shared/MainLayout.razor +++ b/Server/Shared/MainLayout.razor @@ -1,7 +1,8 @@ @using Remotely.Server.Components @using Remotely.Server.Auth @inherits LayoutComponentBase - +@using Microsoft.Extensions.Localization +@inject IStringLocalizer Localizer
    @@ -22,14 +23,14 @@
    -

    Two-Factor Authentication Required

    +

    @Localizer["Two-Factor Authentication Required"]


    -

    Two-factor authentication is required. Click the button below to set up your authenticator app.

    +

    @Localizer["Two-factor authentication is required. Click the button below to set up your authenticator app."]

    - Enable 2FA + @Localizer["Enable 2FA"]

    diff --git a/Server/Shared/NavMenu.razor b/Server/Shared/NavMenu.razor index 3edaacd34..1a36b3e4d 100644 --- a/Server/Shared/NavMenu.razor +++ b/Server/Shared/NavMenu.razor @@ -1,7 +1,15 @@ -@inject IAuthService AuthService +@inject IAuthService AuthService @inject IApplicationConfig AppConfig @inject IDataService DataService - +@using Microsoft.AspNetCore.Builder +@using Microsoft.AspNetCore.Http +@using Microsoft.Extensions.Localization +@using Microsoft.Extensions.Options +@using Remotely.Server.Localization +@using System.Globalization +@inject IStringLocalizer Localizer +@inject IOptions locOptions +@inject NavigationManager Nav -@code { +@code { private bool collapseNavMenu = true; private Dictionary _remoteControlAttributes = new() { ["target"] = "blank", ["href"] = "/RemoteControl" }; + private string _selectlanguage; + public string Selectlanguage + { + get + { + return _selectlanguage; + } + set + { + _selectlanguage = value; + if (CultureInfo.CurrentCulture.Name != value) + { + var uri = new Uri(Nav.Uri) + .GetComponents(UriComponents.PathAndQuery, UriFormat.Unescaped); + var cultureEscaped = Uri.EscapeDataString(value); + var uriEscaped = Uri.EscapeDataString(uri); + + Nav.NavigateTo( + $"Culture/Set?culture={cultureEscaped}&redirectUri={uriEscaped}", + forceLoad: true); + } + } + } + - private RemotelyUser _user; + RemotelyUser _user; private Organization _organization; protected override async Task OnInitializedAsync() @@ -153,6 +197,7 @@ { _organization = await DataService.GetDefaultOrganization(); } + _selectlanguage = CultureInfo.CurrentCulture.Name; } private string NavMenuCssClass => collapseNavMenu ? "collapse" : null; diff --git a/Server/Startup.cs b/Server/Startup.cs index 05c8bd48b..4bcf2e390 100644 --- a/Server/Startup.cs +++ b/Server/Startup.cs @@ -27,6 +27,11 @@ using Microsoft.AspNetCore.Authorization; using Remotely.Server.Auth; using Microsoft.AspNetCore.Http.Extensions; +using Remotely.Server.Localization; +using Microsoft.Extensions.Localization; +using Microsoft.AspNetCore.Localization; +using System.Globalization; +using Microsoft.Extensions.DependencyInjection.Extensions; namespace Remotely.Server { @@ -101,7 +106,13 @@ public void ConfigureServices(IServiceCollection services) }); }); - services.AddRazorPages(); + + + services.AddDistributedMemoryCache(); + services.AddTransient(); + services.AddTransient(); + services.AddLocalization(options => options.ResourcesPath = "Resources"); + services.AddRazorPages().AddDataAnnotationsLocalization(); services.AddServerSideBlazor(); services.AddScoped>(); services.AddDatabaseDeveloperPageExceptionFilter(); @@ -176,6 +187,7 @@ public void ConfigureServices(IServiceCollection services) services.AddScoped(); services.AddScoped(); services.AddScoped(); + } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. @@ -204,7 +216,17 @@ public void Configure(IApplicationBuilder app, app.UseHttpsRedirection(); } } + var provider = new AcceptLanguageCultureProvider(); + var options = new RequestLocalizationOptions + { + DefaultRequestCulture = new RequestCulture(AcceptLanguageCultureProvider.SupperCultureInfos[0]), + SupportedCultures = AcceptLanguageCultureProvider.SupperCultureInfos, + SupportedUICultures = AcceptLanguageCultureProvider.SupperCultureInfos + }; + options.RequestCultureProviders.Add(provider); + app.UseRequestLocalization(options); + app.UseStaticFiles(); app.UseMiddleware(); ConfigureStaticFiles(app, env); diff --git a/Server/_Imports.razor b/Server/_Imports.razor index 5ddf2af0a..e5e4addf6 100644 --- a/Server/_Imports.razor +++ b/Server/_Imports.razor @@ -21,4 +21,5 @@ @using Remotely.Server.Components.TabControl @using System.Collections.Concurrent @using Remotely.Server.Components.Scripts -@using Remotely.Server.Components.TreeView \ No newline at end of file +@using Remotely.Server.Components.TreeView +@using Microsoft.AspNetCore.Mvc.Localization diff --git a/Shared/Models/SavedScript.cs b/Shared/Models/SavedScript.cs index cb15c4b41..d3ea18b0d 100644 --- a/Shared/Models/SavedScript.cs +++ b/Shared/Models/SavedScript.cs @@ -43,7 +43,7 @@ public class SavedScript public bool SendEmailOnError { get; set; } - [EmailAddress] + [EmailAddress(ErrorMessage = "The true field is not a valid e-mail address.")] public string SendErrorEmailTo { get; set; } public ScriptingShell Shell { get; set; } diff --git a/Shared/ViewModels/InviteViewModel.cs b/Shared/ViewModels/InviteViewModel.cs index 24bbc20e4..8240b6ed0 100644 --- a/Shared/ViewModels/InviteViewModel.cs +++ b/Shared/ViewModels/InviteViewModel.cs @@ -8,7 +8,7 @@ public class InviteViewModel public string ID { get; set; } public bool IsAdmin { get; set; } public DateTimeOffset DateSent { get; set; } - [EmailAddress] + [EmailAddress(ErrorMessage = "The true field is not a valid e-mail address.")] public string InvitedUser { get; set; } } }
    TypeTimestampMessageSourceStack Trace@Localizer["Type"]@Localizer["Timestamp"]@Localizer["Message"]@Localizer["Source"]@Localizer["Stack Trace"]