├── assets ├── icon.jpeg ├── logo.jpeg └── panel_screenshot.png ├── translord ├── icon.jpeg ├── ITranslationsCache.cs ├── ILanguageTranslator.cs ├── ITranslationsStore.cs ├── Models │ └── Translation.cs ├── ITranslator.cs ├── TranslatorConfiguration.cs ├── translord.csproj ├── TranslordServiceCollectionExtensions.cs ├── README.md ├── Core │ ├── Translator.cs │ └── FileStore.cs └── Enums │ └── Language.cs ├── examples ├── WebApiWithUI │ ├── my-ui-app │ │ ├── .eslintrc.json │ │ ├── src │ │ │ ├── app │ │ │ │ ├── favicon.ico │ │ │ │ ├── images │ │ │ │ │ └── icon.jpeg │ │ │ │ ├── api │ │ │ │ │ └── axiosInstance.ts │ │ │ │ ├── i18n │ │ │ │ │ ├── settings.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── globals.css │ │ │ │ └── [lng] │ │ │ │ │ ├── layout.tsx │ │ │ │ │ └── page.tsx │ │ │ └── middleware.ts │ │ ├── next.config.mjs │ │ ├── postcss.config.mjs │ │ ├── next-env.d.ts │ │ ├── tailwind.config.ts │ │ ├── public │ │ │ ├── vercel.svg │ │ │ └── next.svg │ │ ├── tsconfig.json │ │ ├── package.json │ │ └── README.md │ ├── WebApi │ │ ├── appsettings.json │ │ ├── WebApi.http │ │ ├── appsettings.Development.json │ │ ├── translations │ │ │ ├── translations.en-gb.json │ │ │ └── translations.pl.json │ │ ├── CustomTranslationsCache.cs │ │ ├── Properties │ │ │ └── launchSettings.json │ │ ├── WebApi.csproj │ │ ├── CustomTranslationsStore.cs │ │ └── Program.cs │ └── README.md └── ConsoleApp │ ├── README.md │ ├── ConsoleApp.csproj │ ├── translations │ └── translations.pl.json │ └── Program.cs ├── translord.DeepL ├── icon.jpeg ├── TranslordDeepLTranslatorServiceCollectionExtensions.cs ├── README.md ├── translord.DeepL.csproj └── DeepLTranslator.cs ├── translord.Manager ├── icon.jpeg ├── Components │ ├── Account │ │ ├── Pages │ │ │ ├── _Imports.razor │ │ │ ├── Manage │ │ │ │ ├── _Imports.razor │ │ │ │ ├── PersonalData.razor │ │ │ │ ├── ResetAuthenticator.razor │ │ │ │ ├── Disable2fa.razor │ │ │ │ ├── GenerateRecoveryCodes.razor │ │ │ │ ├── Index.razor │ │ │ │ ├── DeletePersonalData.razor │ │ │ │ ├── SetPassword.razor │ │ │ │ ├── TwoFactorAuthentication.razor │ │ │ │ ├── ChangePassword.razor │ │ │ │ ├── Email.razor │ │ │ │ └── ExternalLogins.razor │ │ │ ├── InvalidUser.razor │ │ │ ├── InvalidPasswordReset.razor │ │ │ ├── AccessDenied.razor │ │ │ ├── Lockout.razor │ │ │ ├── ForgotPasswordConfirmation.razor │ │ │ ├── ResetPasswordConfirmation.razor │ │ │ ├── ConfirmEmail.razor │ │ │ ├── ConfirmEmailChange.razor │ │ │ ├── RegisterConfirmation.razor │ │ │ ├── ResendEmailConfirmation.razor │ │ │ ├── ForgotPassword.razor │ │ │ ├── LoginWithRecoveryCode.razor │ │ │ ├── LoginWith2fa.razor │ │ │ ├── ResetPassword.razor │ │ │ └── Login.razor │ │ ├── Shared │ │ │ ├── RedirectToLogin.razor │ │ │ ├── ManageLayout.razor │ │ │ ├── ShowRecoveryCodes.razor │ │ │ ├── AccountLayout.razor │ │ │ ├── StatusMessage.razor │ │ │ ├── ManageNavMenu.razor │ │ │ └── ExternalLoginPicker.razor │ │ ├── IdentityUserAccessor.cs │ │ ├── IdentityNoOpEmailSender.cs │ │ ├── IdentityRevalidatingAuthenticationStateProvider.cs │ │ ├── IdentityRedirectManager.cs │ │ └── IdentityComponentsEndpointRouteBuilderExtensions.cs │ ├── Pages │ │ ├── Translations.razor │ │ ├── Home.razor │ │ └── Error.razor │ ├── Routes.razor │ ├── _Imports.razor │ ├── Layout │ │ ├── MainLayout.razor │ │ ├── MainLayout.razor.css │ │ └── NavMenu.razor │ ├── App.razor │ └── Translation │ │ ├── TranslationsStatistics.razor │ │ ├── ImportTranslationsDialog.razor │ │ └── TranslationDialog.razor ├── wwwroot │ ├── favicon.png │ └── app.css ├── Data │ ├── ApplicationUser.cs │ └── TranslordManagerDbContext.cs ├── Models │ └── GroupedTranslations.cs ├── README.md ├── translord.Manager.csproj └── TranslordManagerExtensions.cs ├── translord.RedisCache ├── icon.jpeg ├── README.md ├── TranslordRedisCache.cs ├── translord.RedisCache.csproj └── TranslordRedisCacheServiceCollectionExtensions.cs ├── translord.EntityFramework ├── icon.jpeg ├── README.md ├── TranslordEntityFrameworkServiceCollectionExtensions.cs ├── Data │ ├── Configurations │ │ └── TranslationConfiguration.cs │ └── TranslationsDbContext.cs ├── translord.EntityFramework.csproj └── EfStore.cs ├── translord.EntityFramework.Postgres ├── icon.jpeg ├── TranslationsPostgresDbContext.cs ├── README.md ├── TranslordEntityFrameworkPostgresServiceCollectionExtensions.cs ├── Migrations │ ├── 20240316093826_InitialCreate.cs │ ├── TranslationsPostgresDbContextModelSnapshot.cs │ └── 20240316093826_InitialCreate.Designer.cs └── translord.EntityFramework.Postgres.csproj ├── .vscode ├── launch.json └── tasks.json ├── LICENSE └── translord.sln /assets/icon.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/margosmat/translord/HEAD/assets/icon.jpeg -------------------------------------------------------------------------------- /assets/logo.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/margosmat/translord/HEAD/assets/logo.jpeg -------------------------------------------------------------------------------- /translord/icon.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/margosmat/translord/HEAD/translord/icon.jpeg -------------------------------------------------------------------------------- /examples/WebApiWithUI/my-ui-app/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /translord.DeepL/icon.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/margosmat/translord/HEAD/translord.DeepL/icon.jpeg -------------------------------------------------------------------------------- /assets/panel_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/margosmat/translord/HEAD/assets/panel_screenshot.png -------------------------------------------------------------------------------- /translord.Manager/icon.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/margosmat/translord/HEAD/translord.Manager/icon.jpeg -------------------------------------------------------------------------------- /translord.RedisCache/icon.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/margosmat/translord/HEAD/translord.RedisCache/icon.jpeg -------------------------------------------------------------------------------- /translord.EntityFramework/icon.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/margosmat/translord/HEAD/translord.EntityFramework/icon.jpeg -------------------------------------------------------------------------------- /translord.Manager/Components/Account/Pages/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using translord.Manager.Components.Account.Shared 2 | @layout AccountLayout -------------------------------------------------------------------------------- /translord.Manager/wwwroot/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/margosmat/translord/HEAD/translord.Manager/wwwroot/favicon.png -------------------------------------------------------------------------------- /translord.EntityFramework.Postgres/icon.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/margosmat/translord/HEAD/translord.EntityFramework.Postgres/icon.jpeg -------------------------------------------------------------------------------- /translord.Manager/Components/Account/Pages/Manage/_Imports.razor: -------------------------------------------------------------------------------- 1 | @layout ManageLayout 2 | @attribute [Microsoft.AspNetCore.Authorization.Authorize] -------------------------------------------------------------------------------- /examples/WebApiWithUI/my-ui-app/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/margosmat/translord/HEAD/examples/WebApiWithUI/my-ui-app/src/app/favicon.ico -------------------------------------------------------------------------------- /examples/WebApiWithUI/my-ui-app/next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | 3 | const nextConfig = {}; 4 | 5 | export default nextConfig; 6 | -------------------------------------------------------------------------------- /examples/WebApiWithUI/my-ui-app/src/app/images/icon.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/margosmat/translord/HEAD/examples/WebApiWithUI/my-ui-app/src/app/images/icon.jpeg -------------------------------------------------------------------------------- /translord.Manager/Components/Account/Pages/InvalidUser.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/InvalidUser" 2 | 3 | Invalid user 4 | 5 |

Invalid user

6 | 7 | -------------------------------------------------------------------------------- /examples/WebApiWithUI/my-ui-app/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /examples/WebApiWithUI/WebApi/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /examples/WebApiWithUI/WebApi/WebApi.http: -------------------------------------------------------------------------------- 1 | GET https://localhost:7035/translations/1/label.hello 2 | Accept: application/json 3 | 4 | ### 5 | 6 | GET https://localhost:7035/translations-ef/2/label.hello 7 | Accept: application/json 8 | 9 | ### -------------------------------------------------------------------------------- /translord.Manager/Components/Account/Pages/InvalidPasswordReset.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/InvalidPasswordReset" 2 | 3 | Invalid password reset 4 | 5 |

Invalid password reset

6 |

7 | The password reset link is invalid. 8 |

-------------------------------------------------------------------------------- /examples/WebApiWithUI/my-ui-app/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /translord/ITranslationsCache.cs: -------------------------------------------------------------------------------- 1 | namespace translord; 2 | 3 | public interface ITranslationsCache 4 | { 5 | Task Add(string key, string value); 6 | Task Get(string key); 7 | Task Remove(string key); 8 | Task RemoveAll(List keys); 9 | } -------------------------------------------------------------------------------- /translord.Manager/Data/ApplicationUser.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Identity; 2 | 3 | namespace translord.Manager.Data; 4 | 5 | // Add profile data for application users by adding properties to the ApplicationUser class 6 | public class ApplicationUser : IdentityUser 7 | { 8 | } -------------------------------------------------------------------------------- /translord.Manager/Components/Account/Pages/AccessDenied.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/AccessDenied" 2 | 3 | Access denied 4 | 5 |
6 |

Access denied

7 |

You do not have access to this resource.

8 |
-------------------------------------------------------------------------------- /translord.Manager/Components/Account/Pages/Lockout.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/Lockout" 2 | 3 | Locked out 4 | 5 |
6 |

Locked out

7 |

This account has been locked out, please try again later.

8 |
-------------------------------------------------------------------------------- /translord.Manager/Components/Account/Pages/ForgotPasswordConfirmation.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/ForgotPasswordConfirmation" 2 | 3 | Forgot password confirmation 4 | 5 |

Forgot password confirmation

6 |

7 | Please check your email to reset your password. 8 |

-------------------------------------------------------------------------------- /translord.Manager/Models/GroupedTranslations.cs: -------------------------------------------------------------------------------- 1 | using translord.Models; 2 | 3 | namespace translord.Manager.Models; 4 | 5 | public class GroupedTranslations 6 | { 7 | public required string Key { get; set; } 8 | 9 | public List Translations { get; init; } = []; 10 | } -------------------------------------------------------------------------------- /translord/ILanguageTranslator.cs: -------------------------------------------------------------------------------- 1 | using translord.Enums; 2 | 3 | namespace translord; 4 | 5 | public interface ILanguageTranslator 6 | { 7 | Task Translate(string text, Language from, Language to); 8 | Task> Translate(string text, Language from, List to); 9 | } -------------------------------------------------------------------------------- /examples/WebApiWithUI/my-ui-app/src/app/api/axiosInstance.ts: -------------------------------------------------------------------------------- 1 | 2 | import axios from "axios"; 3 | import https from "https"; 4 | 5 | const httpsAgent = new https.Agent({ 6 | rejectUnauthorized: false 7 | }); 8 | const instance = axios.create({ httpsAgent }) 9 | 10 | export default instance; -------------------------------------------------------------------------------- /examples/ConsoleApp/README.md: -------------------------------------------------------------------------------- 1 | # ConsoleApp example 2 | ConsoleApp uses core `translord` package with `FileStore` and `translord.DeepL` package for AI translations. 3 | 4 | ### Prerequisites 5 | .Net 8 6 | 7 | ### Run 8 | To run the app please run these commands: 9 | ```bash 10 | dotnet restore 11 | dotnet run 12 | ``` -------------------------------------------------------------------------------- /translord.Manager/Components/Account/Pages/ResetPasswordConfirmation.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/ResetPasswordConfirmation" 2 | Reset password confirmation 3 | 4 |

Reset password confirmation

5 |

6 | Your password has been reset. Please click here to log in. 7 |

-------------------------------------------------------------------------------- /translord.Manager/Components/Account/Shared/RedirectToLogin.razor: -------------------------------------------------------------------------------- 1 | @inject NavigationManager NavigationManager 2 | 3 | @code { 4 | 5 | protected override void OnInitialized() 6 | { 7 | NavigationManager.NavigateTo($"Account/Login?returnUrl={Uri.EscapeDataString(NavigationManager.Uri)}", forceLoad: true); 8 | } 9 | 10 | } -------------------------------------------------------------------------------- /translord.Manager/Data/TranslordManagerDbContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Identity.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore; 3 | 4 | namespace translord.Manager.Data; 5 | 6 | public class TranslordManagerDbContext(DbContextOptions options) 7 | : IdentityDbContext(options) 8 | { 9 | } -------------------------------------------------------------------------------- /translord.RedisCache/README.md: -------------------------------------------------------------------------------- 1 | ## translord.RedisCache 2 | translord.RedisCache contains the configuration of the translations cache using Redis. 3 | 4 | ## Configuration 5 | ### Web DI 6 | ```c# 7 | builder.Services.AddTranslordRedisCache(x => 8 | { 9 | x.Server = "localhost"; 10 | x.Port = 6379; 11 | x.Password = "password"; 12 | }); 13 | ``` -------------------------------------------------------------------------------- /translord.Manager/Components/Account/Shared/ManageLayout.razor: -------------------------------------------------------------------------------- 1 | @inherits LayoutComponentBase 2 | @layout AccountLayout 3 | 4 |

Manage your account

5 | 6 |
7 |

Change your account settings

8 |
9 |
10 |
11 | 12 |
13 |
14 | @Body 15 |
16 |
17 |
-------------------------------------------------------------------------------- /translord.Manager/Components/Pages/Translations.razor: -------------------------------------------------------------------------------- 1 | @page "/translations" 2 | @using translord.Manager.Components.Translation 3 | @inject IConfiguration Configuration 4 | 5 | @rendermode RenderMode.InteractiveServer 6 | 7 | @if (Configuration.GetValue("IsTranslordManagerAuthEnabled")) 8 | { 9 | 10 | 11 | 12 | } 13 | else 14 | { 15 | 16 | } 17 | 18 | -------------------------------------------------------------------------------- /examples/WebApiWithUI/WebApi/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "ConnectionStrings": { 9 | "DefaultConnection": "your-db-connection-string" 10 | }, 11 | "BaseAssemblyName": "WebApi", 12 | "DetailedErrors": true, 13 | "DeepLAuthKey": "your-DeepL-auth-key", 14 | "IsTranslordManagerAuthEnabled": false 15 | } 16 | -------------------------------------------------------------------------------- /translord.EntityFramework/README.md: -------------------------------------------------------------------------------- 1 | ## translord.EntityFramework 2 | translord.EntityFramework contains the configuration of the translations store using EF Core. Be sure to reference packages for your chosen db, and run migrations and db update for the `TranslationsDbContext`. 3 | 4 | ## Configuration 5 | ### Web DI 6 | ```c# 7 | services.AddDbContext(o => o.UseNpgsql(connectionString)); // or any other db 8 | services.AddTranslordEfStore(); 9 | ``` -------------------------------------------------------------------------------- /translord.EntityFramework/TranslordEntityFrameworkServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | 3 | namespace translord.EntityFramework; 4 | 5 | public static class TranslordEntityFrameworkServiceCollectionExtensions 6 | { 7 | public static IServiceCollection AddTranslordEfStore(this IServiceCollection services) 8 | { 9 | services.AddTransient(); 10 | return services; 11 | } 12 | } -------------------------------------------------------------------------------- /translord/ITranslationsStore.cs: -------------------------------------------------------------------------------- 1 | using translord.Enums; 2 | 3 | namespace translord; 4 | 5 | public interface ITranslationsStore 6 | { 7 | Task GetSerializedTranslations(Language language); 8 | Task> GetAllKeys(); 9 | TranslatorConfiguration? Config { get; set; } 10 | Task SaveTranslation(string key, Language language, string value); 11 | Task RemoveTranslation(string key); 12 | Task> GetTranslationsCount(); 13 | } -------------------------------------------------------------------------------- /translord/Models/Translation.cs: -------------------------------------------------------------------------------- 1 | using translord.Enums; 2 | 3 | namespace translord.Models; 4 | 5 | public class Translation 6 | { 7 | public required string Key { get; set; } 8 | public required string Value { get; set; } 9 | public Language Language { get; set; } 10 | 11 | public Translation Copy() 12 | { 13 | return new Translation 14 | { 15 | Key = Key, 16 | Value = Value, 17 | Language = Language 18 | }; 19 | } 20 | } -------------------------------------------------------------------------------- /translord.EntityFramework.Postgres/TranslationsPostgresDbContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using translord.EntityFramework.Data; 3 | 4 | namespace translord.EntityFramework.Postgres; 5 | 6 | internal class TranslationsPostgresDbContext : TranslationsDbContext 7 | { 8 | private readonly string _connectionString = ""; 9 | 10 | protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) 11 | { 12 | optionsBuilder.UseNpgsql(_connectionString); 13 | } 14 | } -------------------------------------------------------------------------------- /examples/WebApiWithUI/my-ui-app/src/app/i18n/settings.ts: -------------------------------------------------------------------------------- 1 | export const fallbackLng = "en-GB"; 2 | export const languages = [fallbackLng, "pl", "de", "fr", "ja", "es", "uk", "cs"]; 3 | export const defaultNS = "translation"; 4 | export const cookieName = "i18next"; 5 | 6 | export function getOptions(lng = fallbackLng, ns = defaultNS) { 7 | return { 8 | // debug: true, 9 | supportedLngs: languages, 10 | fallbackLng, 11 | lng, 12 | fallbackNS: defaultNS, 13 | defaultNS, 14 | ns, 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /translord.Manager/Components/Routes.razor: -------------------------------------------------------------------------------- 1 | @using translord.Manager.Components.Account.Shared 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/ConsoleApp/ConsoleApp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /translord.EntityFramework.Postgres/README.md: -------------------------------------------------------------------------------- 1 | ## translord.EntityFramework.Postgres 2 | 3 | translord.EntityFramework.Postgres contains the configuration of the translations store using EF Core with Postgres. 4 | 5 | ## Configuration 6 | 7 | ### Web DI 8 | 9 | ```c# 10 | builder.Services.AddTranslordPostgresStore(options => 11 | options.ConnectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? string.Empty);// Remember to add connection string to your config, or add it from different place (KeyVault/etc.) 12 | ``` -------------------------------------------------------------------------------- /translord.EntityFramework/Data/Configurations/TranslationConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 3 | using translord.Models; 4 | 5 | namespace translord.EntityFramework.Data.Configurations; 6 | 7 | internal class TranslationConfiguration : IEntityTypeConfiguration 8 | { 9 | public void Configure(EntityTypeBuilder builder) 10 | { 11 | builder.HasKey(x => new { x.Key, x.Language }); 12 | builder.Property(x => x.Key).ValueGeneratedNever(); 13 | } 14 | } -------------------------------------------------------------------------------- /translord.Manager/Components/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using System.Net.Http.Json 3 | @using Microsoft.AspNetCore.Components.Authorization 4 | @using Microsoft.AspNetCore.Components.Forms 5 | @using Microsoft.AspNetCore.Components.Routing 6 | @using Microsoft.AspNetCore.Components.Web 7 | @using static Microsoft.AspNetCore.Components.Web.RenderMode 8 | @using Microsoft.AspNetCore.Components.Web.Virtualization 9 | @using Microsoft.JSInterop 10 | @using translord.Manager 11 | @using translord.Manager.Components 12 | @using Microsoft.FluentUI.AspNetCore.Components -------------------------------------------------------------------------------- /translord.Manager/Components/Pages/Home.razor: -------------------------------------------------------------------------------- 1 | @page "/" 2 | @using translord.Manager.Components.Translation 3 | @inject IConfiguration Configuration 4 | 5 | Home 6 | 7 |

translord

8 | 9 | @if (Configuration.GetValue("IsTranslordManagerAuthEnabled")) 10 | { 11 | 12 | 13 | 14 | 15 | 16 |

Log in to see your translations statistics.

17 |
18 |
19 | } 20 | else 21 | { 22 | 23 | } -------------------------------------------------------------------------------- /examples/WebApiWithUI/my-ui-app/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config: Config = { 4 | content: [ 5 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", 6 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}", 8 | ], 9 | theme: { 10 | extend: { 11 | backgroundImage: { 12 | "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", 13 | "gradient-conic": 14 | "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", 15 | }, 16 | }, 17 | }, 18 | plugins: [], 19 | }; 20 | export default config; 21 | -------------------------------------------------------------------------------- /examples/WebApiWithUI/my-ui-app/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /translord.DeepL/TranslordDeepLTranslatorServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | 3 | namespace translord.DeepL; 4 | 5 | public static class TranslordDeepLTranslatorServiceCollectionExtensions 6 | { 7 | public static IServiceCollection AddTranslordDeepLTranslator(this IServiceCollection services, 8 | Action setupAction) 9 | { 10 | var options = new AddTranslordDeepLTranslatorOptions(); 11 | setupAction(options); 12 | services.AddTransient(x => new DeepLTranslator(options)); 13 | return services; 14 | } 15 | } -------------------------------------------------------------------------------- /examples/WebApiWithUI/WebApi/translations/translations.en-gb.json: -------------------------------------------------------------------------------- 1 | { 2 | "label.add": "add", 3 | "label.chooseLanguage": "Choose language", 4 | "message.paragraph2": "Use this sample project as a sandbox.", 5 | "message.paragraph4": "Read the readme, check out the translord nuget packages.", 6 | "message.paragraph5": "If you think that translord is missing some features to be perfect, please add an issue on GH.", 7 | "message.paragraph1": "Hi! I\u0027m glad you\u0027re checking out translord.", 8 | "message.paragraph3": "Add new languages, new labels, try the TMS panel.", 9 | "message.paragraph6": "If you like the project, please leave a star." 10 | } -------------------------------------------------------------------------------- /translord.Manager/Components/Layout/MainLayout.razor: -------------------------------------------------------------------------------- 1 | @inherits LayoutComponentBase 2 | 3 |
4 | 7 | 8 |
9 |
10 | @Body 11 |
12 | 13 | 14 | 15 | 16 |
17 |
18 | 19 |
20 | An unhandled error has occurred. 21 | Reload 22 | 🗙 23 |
-------------------------------------------------------------------------------- /examples/WebApiWithUI/README.md: -------------------------------------------------------------------------------- 1 | # WebApiWithUI example 2 | WebApiWithUI uses core `translord` package with `FileStore`, `translord.DeepL` package for AI translations, `translord.RedisCache` for translations caching and `translord.Manager` with UI panel. This example simulates common use case where we have UI app and API that the UI app calls for data. 3 | 4 | ### Prerequisites 5 | .Net 8 6 | Node.js v20 7 | Redis DB 8 | Postgres DB (when using auth in `translord.Manager`) 9 | 10 | ### Run 11 | To run the apps please run these commands: 12 | 13 | #### Api 14 | ```bash 15 | cd WebApi 16 | dotnet restore 17 | dotnet run 18 | ``` 19 | 20 | #### UI 21 | ```bash 22 | cd my-ui-app 23 | npm i 24 | npm run dev 25 | ``` -------------------------------------------------------------------------------- /examples/WebApiWithUI/my-ui-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./src/*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | -------------------------------------------------------------------------------- /examples/WebApiWithUI/WebApi/translations/translations.pl.json: -------------------------------------------------------------------------------- 1 | { 2 | "label.add": "doda\u0107", 3 | "label.chooseLanguage": "Wybierz j\u0119zyk", 4 | "message.paragraph4": "Przeczytaj readme, sprawd\u017A pakiety translord nuget.", 5 | "message.paragraph2": "U\u017Cyj tego przyk\u0142adowego projektu jako piaskownicy.", 6 | "message.paragraph5": "Je\u015Bli uwa\u017Casz, \u017Ce translordowi brakuje jakiej\u015B funkcji do idea\u0142u, dodaj zg\u0142oszenie na GH.", 7 | "message.paragraph1": "Cze\u015B\u0107! Ciesz\u0119 si\u0119, \u017Ce sprawdzasz translord.", 8 | "message.paragraph3": "Dodaj nowe j\u0119zyki, nowe etykiety, wypr\u00F3buj panel TMS.", 9 | "message.paragraph6": "Je\u015Bli podoba Ci si\u0119 projekt, zostaw gwiazdk\u0119." 10 | } -------------------------------------------------------------------------------- /translord.EntityFramework/Data/TranslationsDbContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using translord.Models; 3 | 4 | namespace translord.EntityFramework.Data; 5 | 6 | public class TranslationsDbContext : DbContext 7 | { 8 | public required DbSet Translations { get; set; } 9 | 10 | public TranslationsDbContext() {} 11 | 12 | public TranslationsDbContext(DbContextOptions options) : base(options) 13 | { 14 | Database.EnsureCreated(); 15 | } 16 | 17 | protected override void OnModelCreating(ModelBuilder modelBuilder) 18 | { 19 | base.OnModelCreating(modelBuilder); 20 | modelBuilder.ApplyConfigurationsFromAssembly(typeof(TranslationsDbContext).Assembly); 21 | } 22 | } -------------------------------------------------------------------------------- /translord.Manager/Components/Account/IdentityUserAccessor.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Identity; 2 | using translord.Manager.Data; 3 | 4 | namespace translord.Manager.Components.Account; 5 | 6 | internal sealed class IdentityUserAccessor( 7 | UserManager userManager, 8 | IdentityRedirectManager redirectManager) 9 | { 10 | public async Task GetRequiredUserAsync(HttpContext context) 11 | { 12 | var user = await userManager.GetUserAsync(context.User); 13 | 14 | if (user is null) 15 | { 16 | redirectManager.RedirectToWithStatus("Account/InvalidUser", 17 | $"Error: Unable to load user with ID '{userManager.GetUserId(context.User)}'.", context); 18 | } 19 | 20 | return user; 21 | } 22 | } -------------------------------------------------------------------------------- /examples/ConsoleApp/translations/translations.pl.json: -------------------------------------------------------------------------------- 1 | { 2 | "message.paragraph1": "Hej, to jest pierwszy akapit przykładowego tekstu,", 3 | "message.paragraph2": "(uzywasz domyślnego języka, zeby zobaczyć tłumaczenia wykonane przy uzyciu DeepL, wybierz inny język),", 4 | "message.paragraph3": "ta aplikacja słuzy do zademonstrowania jak mogłoby wyglądać uzycie translord", 5 | "message.paragraph4": "w aplikacjach desktopowych, konsolowych, czy grach stworzonych przy uzyciu Unity.", 6 | "message.paragraph5": "O ile tłumaczenie wszystkich tekstów w aplikacji podczas jej startu nie wydaje się zbyt efektywne,", 7 | "message.paragraph6": "mogłoby się sprawdzić jako część build process'u, przynajmniej do czasu stworzenia", 8 | "message.paragraph7": "oficjalnego Github Action dla translord 😅 Dzięki za uzycie translord, miłego programowania!" 9 | } -------------------------------------------------------------------------------- /examples/WebApiWithUI/WebApi/CustomTranslationsCache.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | using translord; 3 | 4 | namespace WebApi; 5 | 6 | public class CustomTranslationsCache(ConcurrentDictionary cache) : ITranslationsCache 7 | { 8 | public Task Add(string key, string value) 9 | { 10 | cache.TryAdd(key, value); 11 | return Task.CompletedTask; 12 | } 13 | 14 | public Task Get(string key) 15 | { 16 | cache.TryGetValue(key, out var value); 17 | return Task.FromResult(value); 18 | } 19 | 20 | public Task Remove(string key) 21 | { 22 | cache.TryRemove(key, out _); 23 | return Task.CompletedTask; 24 | } 25 | 26 | public Task RemoveAll(List keys) 27 | { 28 | throw new NotImplementedException(); 29 | } 30 | } -------------------------------------------------------------------------------- /translord.Manager/Components/Account/Shared/ShowRecoveryCodes.razor: -------------------------------------------------------------------------------- 1 | 2 |

Recovery codes

3 | 11 |
12 |
13 | @foreach (var recoveryCode in RecoveryCodes) 14 | { 15 |
16 | @recoveryCode 17 |
18 | } 19 |
20 |
21 | 22 | @code { 23 | [Parameter] public string[] RecoveryCodes { get; set; } = []; 24 | 25 | [Parameter] public string? StatusMessage { get; set; } 26 | } -------------------------------------------------------------------------------- /translord.Manager/Components/App.razor: -------------------------------------------------------------------------------- 1 | @inject IConfiguration configuration; 2 | 3 | @code 4 | { 5 | string getBundledStylesPath => configuration["BaseAssemblyName"] + ".styles.css"; 6 | } 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /examples/WebApiWithUI/my-ui-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-ui-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev -p 3001", 7 | "build": "next build", 8 | "start": "next start -p 3001", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "accept-language": "^3.0.18", 13 | "axios": "^1.7.2", 14 | "i18next": "^23.11.5", 15 | "i18next-http-backend": "^2.5.2", 16 | "next": "14.2.3", 17 | "react": "^18", 18 | "react-dom": "^18", 19 | "react-i18next": "^14.1.2" 20 | }, 21 | "devDependencies": { 22 | "@types/node": "^20", 23 | "@types/react": "^18", 24 | "@types/react-dom": "^18", 25 | "eslint": "^8", 26 | "eslint-config-next": "14.2.3", 27 | "postcss": "^8", 28 | "tailwindcss": "^3.4.1", 29 | "typescript": "^5" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /examples/WebApiWithUI/my-ui-app/src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --foreground-rgb: 0, 0, 0; 7 | --background-start-rgb: 214, 219, 220; 8 | --background-end-rgb: 255, 255, 255; 9 | } 10 | 11 | @media (prefers-color-scheme: dark) { 12 | :root { 13 | --foreground-rgb: 255, 255, 255; 14 | --background-start-rgb: 0, 0, 0; 15 | --background-end-rgb: 0, 0, 0; 16 | } 17 | } 18 | 19 | body { 20 | color: rgb(var(--foreground-rgb)); 21 | background: linear-gradient( 22 | to bottom, 23 | transparent, 24 | rgb(var(--background-end-rgb)) 25 | ) 26 | rgb(var(--background-start-rgb)); 27 | } 28 | 29 | @layer utilities { 30 | .text-balance { 31 | text-wrap: balance; 32 | } 33 | } 34 | 35 | .dropdown:hover .dropdown-menu { 36 | display: block; 37 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch (console)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | "program": "${workspaceFolder}/examples/ConsoleApp/bin/Debug/net8.0/ConsoleApp.dll", 13 | "args": [], 14 | "cwd": "${workspaceFolder}/examples/ConsoleApp", 15 | "console": "internalConsole", 16 | "stopAtEntry": false 17 | }, 18 | { 19 | "name": ".NET Core Attach", 20 | "type": "coreclr", 21 | "request": "attach" 22 | } 23 | ] 24 | } -------------------------------------------------------------------------------- /translord.Manager/Components/Account/Shared/AccountLayout.razor: -------------------------------------------------------------------------------- 1 | @inherits LayoutComponentBase 2 | @layout translord.Manager.Components.Layout.MainLayout 3 | @inject NavigationManager NavigationManager 4 | 5 | @if (HttpContext is null) 6 | { 7 |

Loading...

8 | } 9 | else 10 | { 11 | @Body 12 | } 13 | 14 | @code { 15 | [CascadingParameter] private HttpContext? HttpContext { get; set; } 16 | 17 | protected override void OnParametersSet() 18 | { 19 | if (HttpContext is null) 20 | { 21 | // If this code runs, we're currently rendering in interactive mode, so there is no HttpContext. 22 | // The identity pages need to set cookies, so they require an HttpContext. To achieve this we 23 | // must transition back from interactive mode to a server-rendered page. 24 | NavigationManager.Refresh(forceReload: true); 25 | } 26 | } 27 | 28 | } -------------------------------------------------------------------------------- /translord/ITranslator.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using translord.Enums; 3 | using translord.Models; 4 | 5 | namespace translord; 6 | 7 | public interface ITranslator 8 | { 9 | bool IsTranslationSupported { get; } 10 | Task GetTranslation(string key, Language language); 11 | Task> GetAllTranslations(Language? language = null); 12 | Task GetAllTranslationsRawJson(Language language); 13 | List GetSupportedLanguages(); 14 | Language? GetDefaultLanguage(); 15 | Task SaveTranslation(string key, Language language, string value); 16 | Task RemoveTranslation(string key); 17 | Task> GetTranslationsCount(); 18 | Task Translate(string text, Language from, Language to); 19 | Task> Translate(string text, Language from, List to); 20 | Task ImportTranslations(JsonDocument json, Language language); 21 | } -------------------------------------------------------------------------------- /translord.RedisCache/TranslordRedisCache.cs: -------------------------------------------------------------------------------- 1 | using StackExchange.Redis; 2 | 3 | namespace translord.RedisCache; 4 | 5 | internal class TranslordRedisCache(IConnectionMultiplexer redis) : ITranslationsCache 6 | { 7 | public async Task Add(string key, string value) 8 | { 9 | var db = redis.GetDatabase(); 10 | await db.StringSetAsync(key, value); 11 | } 12 | 13 | public async Task Get(string key) 14 | { 15 | var db = redis.GetDatabase(); 16 | return await db.StringGetAsync(key); 17 | } 18 | 19 | public async Task Remove(string key) 20 | { 21 | var db = redis.GetDatabase(); 22 | await db.KeyDeleteAsync(key); 23 | } 24 | 25 | public async Task RemoveAll(List keys) 26 | { 27 | var db = redis.GetDatabase(); 28 | foreach (var key in keys) 29 | { 30 | await db.KeyDeleteAsync(key); 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /translord.Manager/Components/Account/Shared/StatusMessage.razor: -------------------------------------------------------------------------------- 1 | @if (!string.IsNullOrEmpty(DisplayMessage)) 2 | { 3 | var statusMessageClass = DisplayMessage.StartsWith("Error") ? "danger" : "success"; 4 | 7 | } 8 | 9 | @code { 10 | private string? messageFromCookie; 11 | 12 | [Parameter] public string? Message { get; set; } 13 | 14 | [CascadingParameter] private HttpContext HttpContext { get; set; } = default!; 15 | 16 | private string? DisplayMessage => Message ?? messageFromCookie; 17 | 18 | protected override void OnInitialized() 19 | { 20 | messageFromCookie = HttpContext.Request.Cookies[IdentityRedirectManager.StatusCookieName]; 21 | 22 | if (messageFromCookie is not null) 23 | { 24 | HttpContext.Response.Cookies.Delete(IdentityRedirectManager.StatusCookieName); 25 | } 26 | } 27 | 28 | } -------------------------------------------------------------------------------- /examples/WebApiWithUI/my-ui-app/src/app/[lng]/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import "../globals.css"; 4 | import { dir } from 'i18next' 5 | import { languages } from '../i18n/settings' 6 | 7 | export async function generateStaticParams() { 8 | return languages.map((lng) => ({ lng })) 9 | } 10 | 11 | const inter = Inter({ subsets: ["latin"] }); 12 | 13 | export const metadata: Metadata = { 14 | title: "Create Next App", 15 | description: "Generated by create next app", 16 | }; 17 | 18 | type RootLayoutProps = { 19 | params: { 20 | lng: string; 21 | }; 22 | }; 23 | 24 | export default function RootLayout({ 25 | children, 26 | params: { 27 | lng 28 | } 29 | }: Readonly<{ 30 | children: React.ReactNode; 31 | }> & RootLayoutProps) { 32 | return ( 33 | 34 | {children} 35 | 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /translord/TranslatorConfiguration.cs: -------------------------------------------------------------------------------- 1 | using translord.Core; 2 | using translord.Enums; 3 | 4 | namespace translord; 5 | 6 | public record TranslatorConfigurationOptions 7 | { 8 | public IList SupportedLanguages { get; set; } 9 | public Language? DefaultLanguage { get; set; } 10 | } 11 | 12 | public class TranslatorConfiguration( 13 | TranslatorConfigurationOptions options, 14 | ITranslationsStore store, 15 | ILanguageTranslator? languageTranslator = null) 16 | { 17 | public IList SupportedLanguages { get; } = options.SupportedLanguages; 18 | public Language? DefaultLanguage { get; } = options.DefaultLanguage; 19 | private ITranslationsStore TranslationsStore { get; } = store; 20 | private ILanguageTranslator? LanguageTranslator { get; } = languageTranslator; 21 | 22 | public ITranslator CreateTranslator() 23 | { 24 | TranslationsStore.Config = this; 25 | return new Translator(this, TranslationsStore, LanguageTranslator); 26 | } 27 | } -------------------------------------------------------------------------------- /translord.DeepL/README.md: -------------------------------------------------------------------------------- 1 | ## translord.DeepL 2 | translord.DeepL contains the logic responsible for translating texts using the [DeepL API](https://www.deepl.com/pro-api?cta=header-pro-api). To use this package you will need to register and obtain their API key. You can register with free account and use 500k characters translations per month. 3 | 4 | ## Configuration 5 | ### Web DI 6 | ```c# 7 | builder.Services.AddTranslordDeepLTranslator(options => 8 | { 9 | options.AuthKey = builder.Configuration["DeepLAuthKey"]; // Remember to add auth key to your config, or add it from different place (KeyVault/etc.) 10 | }); 11 | ``` 12 | 13 | ### Console app 14 | ```c# 15 | var deeplTranslator = new DeepLTranslator(new AddTranslordDeepLTranslatorOptions { AuthKey = "your-auth-key" }); 16 | var translator = 17 | new TranslatorConfiguration( 18 | new TranslatorConfigurationOptions { SupportedLanguages = supportedLanguages, DefaultLanguage = Language.English }, 19 | new FileStore(new FileStoreOptions { TranslationsPath = path }, null), 20 | deeplTranslator).CreateTranslator(); 21 | ``` -------------------------------------------------------------------------------- /translord.EntityFramework.Postgres/TranslordEntityFrameworkPostgresServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using translord.EntityFramework.Data; 4 | 5 | namespace translord.EntityFramework.Postgres; 6 | 7 | public record AddTranslordPostgresStoreOptions 8 | { 9 | public string? ConnectionString { get; set; } 10 | } 11 | 12 | public static class TranslordEntityFrameworkPostgresServiceCollectionExtensions 13 | { 14 | public static IServiceCollection AddTranslordPostgresStore(this IServiceCollection services, 15 | Action setupAction) 16 | { 17 | var options = new AddTranslordPostgresStoreOptions(); 18 | setupAction(options); 19 | var connectionString = options.ConnectionString ?? 20 | throw new ArgumentNullException(nameof(options.ConnectionString)); 21 | services.AddDbContext(o => o.UseNpgsql(connectionString)); 22 | services.AddTranslordEfStore(); 23 | return services; 24 | } 25 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Mateusz Margos 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /translord/translord.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | translord 8 | 0.1.4 9 | Mateusz Margos 10 | translord 11 | translord - simple TMS to get your translations up and running in no time. 12 | https://github.com/margosmat/translord 13 | MIT 14 | README.md 15 | icon.jpeg 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /translord.Manager/Components/Account/Pages/Manage/PersonalData.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/Manage/PersonalData" 2 | 3 | @inject IdentityUserAccessor UserAccessor 4 | 5 | Personal Data 6 | 7 | 8 |

Personal Data

9 | 10 |
11 |
12 |

Your account contains personal data that you have given us. This page allows you to download or delete that data.

13 |

14 | Deleting this data will permanently remove your account, and this cannot be recovered. 15 |

16 |
17 | 18 | 19 | 20 |

21 | Delete 22 |

23 |
24 |
25 | 26 | @code { 27 | [CascadingParameter] private HttpContext HttpContext { get; set; } = default!; 28 | 29 | protected override async Task OnInitializedAsync() 30 | { 31 | _ = await UserAccessor.GetRequiredUserAsync(HttpContext); 32 | } 33 | 34 | } -------------------------------------------------------------------------------- /translord.DeepL/translord.DeepL.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | translord.DeepL 8 | 0.1.0 9 | Mateusz Margos 10 | translord.DeepL 11 | translord.DeepL - package using DeepL for texts translation in translord. 12 | https://github.com/margosmat/translord/tree/main/translord.DeepL 13 | MIT 14 | README.md 15 | icon.jpeg 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /translord.EntityFramework.Postgres/Migrations/20240316093826_InitialCreate.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace translord.EntityFramework.Postgres.Migrations 6 | { 7 | /// 8 | public partial class InitialCreate : Migration 9 | { 10 | /// 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | migrationBuilder.CreateTable( 14 | name: "Translations", 15 | columns: table => new 16 | { 17 | Key = table.Column(type: "text", nullable: false), 18 | Language = table.Column(type: "integer", nullable: false), 19 | Value = table.Column(type: "text", nullable: false) 20 | }, 21 | constraints: table => 22 | { 23 | table.PrimaryKey("PK_Translations", x => new { x.Key, x.Language }); 24 | }); 25 | } 26 | 27 | /// 28 | protected override void Down(MigrationBuilder migrationBuilder) 29 | { 30 | migrationBuilder.DropTable( 31 | name: "Translations"); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples/WebApiWithUI/WebApi/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:52364", 8 | "sslPort": 44384 9 | } 10 | }, 11 | "profiles": { 12 | "http": { 13 | "commandName": "Project", 14 | "dotnetRunMessages": true, 15 | "launchBrowser": true, 16 | "launchUrl": "swagger", 17 | "applicationUrl": "http://localhost:5238", 18 | "environmentVariables": { 19 | "ASPNETCORE_ENVIRONMENT": "Development" 20 | } 21 | }, 22 | "https": { 23 | "commandName": "Project", 24 | "dotnetRunMessages": true, 25 | "launchBrowser": true, 26 | "launchUrl": "swagger", 27 | "applicationUrl": "https://localhost:7035;http://localhost:5238", 28 | "environmentVariables": { 29 | "ASPNETCORE_ENVIRONMENT": "Development" 30 | } 31 | }, 32 | "IIS Express": { 33 | "commandName": "IISExpress", 34 | "launchBrowser": true, 35 | "launchUrl": "swagger", 36 | "environmentVariables": { 37 | "ASPNETCORE_ENVIRONMENT": "Development" 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /translord.RedisCache/translord.RedisCache.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | translord.RedisCache 8 | 0.1.0 9 | Mateusz Margos 10 | translord.RedisCache 11 | translord.RedisCache - package containing configuration of the translations cache using Redis for translord. 12 | https://github.com/margosmat/translord/tree/main/translord.RedisCache 13 | MIT 14 | README.md 15 | icon.jpeg 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /translord.DeepL/DeepLTranslator.cs: -------------------------------------------------------------------------------- 1 | using DeepL; 2 | using translord.Enums; 3 | 4 | namespace translord.DeepL; 5 | 6 | public record AddTranslordDeepLTranslatorOptions 7 | { 8 | public string? AuthKey { get; set; } 9 | } 10 | 11 | public sealed class DeepLTranslator : ILanguageTranslator 12 | { 13 | private readonly Translator _translator; 14 | 15 | public DeepLTranslator(AddTranslordDeepLTranslatorOptions options) 16 | { 17 | var authKey = options.AuthKey ?? throw new ArgumentNullException(nameof(options.AuthKey)); 18 | _translator = new Translator(authKey); 19 | } 20 | 21 | public async Task Translate(string text, Language from, Language to) 22 | { 23 | var result = await _translator.TranslateTextAsync([text], from.GetSourceIsoCode(), to.GetIsoCode()); 24 | return result[0].Text; 25 | } 26 | 27 | public async Task> Translate(string text, Language from, List to) 28 | { 29 | var translations = new List(); 30 | foreach (var lang in to) 31 | { 32 | var result = await _translator.TranslateTextAsync([text], from.GetSourceIsoCode(), lang.GetIsoCode()); 33 | translations.Add(result[0].Text); 34 | } 35 | 36 | return translations; 37 | } 38 | } -------------------------------------------------------------------------------- /translord.RedisCache/TranslordRedisCacheServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using StackExchange.Redis; 3 | 4 | namespace translord.RedisCache; 5 | 6 | public record AddTranslordRedisCacheOptions 7 | { 8 | public string? Server { get; set; } 9 | public int Port { get; set; } 10 | public string? Password { get; set; } 11 | } 12 | 13 | public static class TranslordRedisCacheServiceCollectionExtensions 14 | { 15 | public static IServiceCollection AddTranslordRedisCache(this IServiceCollection services, 16 | Action setupAction) 17 | { 18 | var options = new AddTranslordRedisCacheOptions(); 19 | setupAction(options); 20 | var server = options.Server ?? throw new ArgumentNullException(nameof(options.Server)); 21 | var configuration = new ConfigurationOptions 22 | { 23 | EndPoints = new EndPointCollection 24 | { 25 | { server, options.Port } 26 | }, 27 | Password = options.Password 28 | }; 29 | services.AddSingleton(x => ConnectionMultiplexer.Connect(configuration)); 30 | services.AddTransient(); 31 | return services; 32 | } 33 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/translord.sln", 11 | "/property:GenerateFullPaths=true", 12 | "/consoleloggerparameters:NoSummary;ForceNoAlign" 13 | ], 14 | "problemMatcher": "$msCompile" 15 | }, 16 | { 17 | "label": "publish", 18 | "command": "dotnet", 19 | "type": "process", 20 | "args": [ 21 | "publish", 22 | "${workspaceFolder}/translord.sln", 23 | "/property:GenerateFullPaths=true", 24 | "/consoleloggerparameters:NoSummary;ForceNoAlign" 25 | ], 26 | "problemMatcher": "$msCompile" 27 | }, 28 | { 29 | "label": "watch", 30 | "command": "dotnet", 31 | "type": "process", 32 | "args": [ 33 | "watch", 34 | "run", 35 | "--project", 36 | "${workspaceFolder}/translord.sln" 37 | ], 38 | "problemMatcher": "$msCompile" 39 | } 40 | ] 41 | } -------------------------------------------------------------------------------- /translord.EntityFramework/translord.EntityFramework.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | translord.EntityFramework 8 | 0.1.0 9 | Mateusz Margos 10 | translord.EntityFramework 11 | translord.EntityFramework - package containing configuration of the translations store using EF Core for translord. 12 | https://github.com/margosmat/translord/tree/main/translord.EntityFramework 13 | MIT 14 | README.md 15 | icon.jpeg 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /translord.Manager/Components/Pages/Error.razor: -------------------------------------------------------------------------------- 1 | @page "/Error" 2 | @using System.Diagnostics 3 | 4 | Error 5 | 6 |

Error.

7 |

An error occurred while processing your request.

8 | 9 | @if (ShowRequestId) 10 | { 11 |

12 | Request ID: @RequestId 13 |

14 | } 15 | 16 |

Development Mode

17 |

18 | Swapping to Development environment will display more detailed information about the error that occurred. 19 |

20 |

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

26 | 27 | @code{ 28 | [CascadingParameter] private HttpContext? HttpContext { get; set; } 29 | 30 | private string? RequestId { get; set; } 31 | private bool ShowRequestId => !string.IsNullOrEmpty(RequestId); 32 | 33 | protected override void OnInitialized() => 34 | RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier; 35 | 36 | } -------------------------------------------------------------------------------- /translord.Manager/Components/Account/IdentityNoOpEmailSender.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Identity; 2 | using Microsoft.AspNetCore.Identity.UI.Services; 3 | using translord.Manager.Data; 4 | 5 | namespace translord.Manager.Components.Account; 6 | 7 | // Remove the "else if (EmailSender is IdentityNoOpEmailSender)" block from RegisterConfirmation.razor after updating with a real implementation. 8 | internal sealed class IdentityNoOpEmailSender : IEmailSender 9 | { 10 | private readonly IEmailSender emailSender = new NoOpEmailSender(); 11 | 12 | public Task SendConfirmationLinkAsync(ApplicationUser user, string email, string confirmationLink) => 13 | emailSender.SendEmailAsync(email, "Confirm your email", 14 | $"Please confirm your account by clicking here."); 15 | 16 | public Task SendPasswordResetLinkAsync(ApplicationUser user, string email, string resetLink) => 17 | emailSender.SendEmailAsync(email, "Reset your password", 18 | $"Please reset your password by clicking here."); 19 | 20 | public Task SendPasswordResetCodeAsync(ApplicationUser user, string email, string resetCode) => 21 | emailSender.SendEmailAsync(email, "Reset your password", 22 | $"Please reset your password using the following code: {resetCode}"); 23 | } -------------------------------------------------------------------------------- /examples/WebApiWithUI/my-ui-app/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /translord.Manager/Components/Account/Shared/ManageNavMenu.razor: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Identity 2 | @using translord.Manager.Data 3 | 4 | @inject SignInManager SignInManager 5 | 6 | 29 | 30 | @code { 31 | private bool hasExternalLogins; 32 | 33 | protected override async Task OnInitializedAsync() 34 | { 35 | hasExternalLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync()).Any(); 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /examples/WebApiWithUI/WebApi/WebApi.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | true 8 | 9 | 10 | 11 | 12 | 13 | 14 | all 15 | runtime; build; native; contentfiles; analyzers; buildtransitive 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /examples/WebApiWithUI/my-ui-app/src/middleware.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server' 2 | import type { NextRequest } from 'next/server' 3 | import acceptLanguage from 'accept-language' 4 | import { fallbackLng, languages, cookieName } from './app/i18n/settings' 5 | 6 | acceptLanguage.languages(languages) 7 | 8 | export const config = { 9 | // matcher: '/:lng*' 10 | matcher: ['/((?!api|_next/static|_next/image|assets|favicon.ico|sw.js|site.webmanifest).*)'] 11 | } 12 | 13 | export function middleware(req: NextRequest) { 14 | let lng 15 | if (req.cookies.has(cookieName)) lng = acceptLanguage.get(req.cookies.get(cookieName)?.value) 16 | if (!lng) lng = acceptLanguage.get(req.headers.get('Accept-Language')) 17 | if (!lng) lng = fallbackLng 18 | 19 | // Redirect if lng in path is not supported 20 | if ( 21 | !languages.some(loc => req.nextUrl.pathname.startsWith(`/${loc}`)) && 22 | !req.nextUrl.pathname.startsWith('/_next') 23 | ) { 24 | return NextResponse.redirect(new URL(`/${lng}${req.nextUrl.pathname}`, req.url)) 25 | } 26 | 27 | if (req.headers.has('referer')) { 28 | const refererUrl = new URL(req.headers.get('referer') ?? '') 29 | const lngInReferer = languages.find((l) => refererUrl.pathname.startsWith(`/${l}`)) 30 | const response = NextResponse.next() 31 | if (lngInReferer) response.cookies.set(cookieName, lngInReferer) 32 | return response 33 | } 34 | 35 | return NextResponse.next() 36 | } -------------------------------------------------------------------------------- /translord.Manager/README.md: -------------------------------------------------------------------------------- 1 | ## translord.Manager 2 | translord.Manager contains the TMS admin panel. In this panel you can manage/translate/import translations. 3 | 4 | ## Configuration 5 | ### Web DI 6 | ```c# 7 | builder.Services.AddDbContext(options => 8 | options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection") ?? string.Empty, 9 | b => b.MigrationsAssembly("WebApi"))); 10 | builder.AddTranslordManager(); 11 | ``` 12 | Be sure to run the migrations for the translord.Manager, so that the database can be created: 13 | ```bash 14 | dotnet ef migrations add -c TranslordManagerDbContext Init 15 | dotnet ef database update -c TranslordManagerDbContext 16 | ``` 17 | 18 | ### Manager configuration 19 | traslord.Manager reads configuration from the main project appsettings.json, there are 2 options that could be configured: 20 | 21 | #### BaseAssemblyName 22 | ```json 23 | "BaseAssemblyName": "WebApi" 24 | ``` 25 | 26 | Be sure to add it, so that styles for translord.Manager can be applied correctly. 27 | 28 | #### IsTranslordManagerAuthEnabled 29 | ```json 30 | "IsTranslordManagerAuthEnabled": true 31 | ``` 32 | 33 | translord.Manager has simple auth implemented from Blazor WebApp template. To use the panel without the auth, leave the property empty, or `false`. If you'd like the auth in the translord.Manager, set it to true. I plan to extend the functionality of the auth in the future, so that there will be role based auth, admin with possibility of adding new users, email account confirmation, etc. -------------------------------------------------------------------------------- /examples/WebApiWithUI/my-ui-app/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3001](http://localhost:3001) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 37 | -------------------------------------------------------------------------------- /translord.Manager/Components/Account/Pages/ConfirmEmail.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/ConfirmEmail" 2 | 3 | @using System.Text 4 | @using Microsoft.AspNetCore.Identity 5 | @using Microsoft.AspNetCore.WebUtilities 6 | @using translord.Manager.Data 7 | 8 | @inject UserManager UserManager 9 | @inject IdentityRedirectManager RedirectManager 10 | 11 | Confirm email 12 | 13 |

Confirm email

14 | 15 | 16 | @code { 17 | private string? statusMessage; 18 | 19 | [CascadingParameter] private HttpContext HttpContext { get; set; } = default!; 20 | 21 | [SupplyParameterFromQuery] private string? UserId { get; set; } 22 | 23 | [SupplyParameterFromQuery] private string? Code { get; set; } 24 | 25 | protected override async Task OnInitializedAsync() 26 | { 27 | if (UserId is null || Code is null) 28 | { 29 | RedirectManager.RedirectTo(""); 30 | } 31 | 32 | var user = await UserManager.FindByIdAsync(UserId); 33 | if (user is null) 34 | { 35 | HttpContext.Response.StatusCode = StatusCodes.Status404NotFound; 36 | statusMessage = $"Error loading user with ID {UserId}"; 37 | } 38 | else 39 | { 40 | var code = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(Code)); 41 | var result = await UserManager.ConfirmEmailAsync(user, code); 42 | statusMessage = result.Succeeded ? "Thank you for confirming your email." : "Error confirming your email."; 43 | } 44 | } 45 | 46 | } -------------------------------------------------------------------------------- /translord.Manager/Components/Translation/TranslationsStatistics.razor: -------------------------------------------------------------------------------- 1 | @using translord.Enums 2 | @inject ITranslator Translator 3 | 4 |

Translations statistics:

5 | @if (_baseNumber == 0) 6 | { 7 |

Start adding translations to see statistics.

8 | } 9 | else 10 | { 11 | @foreach (var (lang, count) in _translationsCount) 12 | { 13 | 14 | @lang.GetName() - @(Math.Min(count * 100 / _baseNumber, 100))% 15 | 16 | 20 | 21 | } 22 | } 23 | 24 | @code { 25 | List<(Language lang, int count)> _translationsCount = new(); 26 | int _baseNumber; 27 | 28 | protected override async Task OnInitializedAsync() 29 | { 30 | await base.OnInitializedAsync(); 31 | var defaultLanguage = Translator.GetDefaultLanguage(); 32 | _translationsCount = await Translator.GetTranslationsCount(); 33 | _translationsCount.Sort((a, b) => 34 | { 35 | if (defaultLanguage != null) return a.lang == defaultLanguage.Value ? -1 : b.lang == defaultLanguage.Value ? 1 : b.count - a.count; 36 | return b.count - a.count; 37 | }); 38 | _baseNumber = defaultLanguage.HasValue ? _translationsCount.Find(x => x.lang == defaultLanguage).count : _translationsCount.MaxBy(x => x.count).count; 39 | } 40 | 41 | } -------------------------------------------------------------------------------- /translord.EntityFramework.Postgres/Migrations/TranslationsPostgresDbContextModelSnapshot.cs: -------------------------------------------------------------------------------- 1 | // 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Infrastructure; 4 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 5 | using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; 6 | using translord.EntityFramework.Postgres; 7 | 8 | #nullable disable 9 | 10 | namespace translord.EntityFramework.Postgres.Migrations 11 | { 12 | [DbContext(typeof(TranslationsPostgresDbContext))] 13 | partial class TranslationsPostgresDbContextModelSnapshot : ModelSnapshot 14 | { 15 | protected override void BuildModel(ModelBuilder modelBuilder) 16 | { 17 | #pragma warning disable 612, 618 18 | modelBuilder 19 | .HasAnnotation("ProductVersion", "8.0.3") 20 | .HasAnnotation("Relational:MaxIdentifierLength", 63); 21 | 22 | NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); 23 | 24 | modelBuilder.Entity("translord.Models.Translation", b => 25 | { 26 | b.Property("Key") 27 | .HasColumnType("text"); 28 | 29 | b.Property("Language") 30 | .HasColumnType("integer"); 31 | 32 | b.Property("Value") 33 | .IsRequired() 34 | .HasColumnType("text"); 35 | 36 | b.HasKey("Key", "Language"); 37 | 38 | b.ToTable("Translations"); 39 | }); 40 | #pragma warning restore 612, 618 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /translord.Manager/Components/Account/Shared/ExternalLoginPicker.razor: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Authentication 2 | @using Microsoft.AspNetCore.Identity 3 | @using translord.Manager.Data 4 | 5 | @inject SignInManager SignInManager 6 | @inject IdentityRedirectManager RedirectManager 7 | 8 | @if (externalLogins.Length == 0) 9 | { 10 |
11 |

12 | There are no external authentication services configured. See this 13 | 14 | article 15 | about setting up this ASP.NET application to support logging in via external services 16 | . 17 |

18 |
19 | } 20 | else 21 | { 22 |
23 |
24 | 25 | 26 |

27 | @foreach (var provider in externalLogins) 28 | { 29 | 30 | } 31 |

32 |
33 |
34 | } 35 | 36 | @code { 37 | private AuthenticationScheme[] externalLogins = []; 38 | 39 | [SupplyParameterFromQuery] private string? ReturnUrl { get; set; } 40 | 41 | protected override async Task OnInitializedAsync() 42 | { 43 | externalLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync()).ToArray(); 44 | } 45 | 46 | } -------------------------------------------------------------------------------- /examples/WebApiWithUI/WebApi/CustomTranslationsStore.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using translord; 3 | using translord.Enums; 4 | 5 | namespace WebApi; 6 | 7 | public class CustomTranslationsStore : ITranslationsStore 8 | { 9 | public Task GetSerializedTranslations(Language language) 10 | { 11 | switch (language) 12 | { 13 | case Language.EnglishBritish: 14 | return Task.FromResult(JsonSerializer.Serialize(new Dictionary 15 | { 16 | {"label.test", "Test"}, 17 | {"label.hello", "Hello"} 18 | })); 19 | case Language.Polish: 20 | return Task.FromResult(JsonSerializer.Serialize(new Dictionary 21 | { 22 | {"label.test", "Testowanko"}, 23 | {"label.hello", "Hejo"} 24 | })); 25 | case Language.German: 26 | default: 27 | return Task.FromResult(string.Empty); 28 | } 29 | } 30 | 31 | public Task> GetAllKeys() 32 | { 33 | return Task.FromResult(new List {"label.test", "label.hello"}); 34 | } 35 | 36 | TranslatorConfiguration? ITranslationsStore.Config { get; set; } 37 | public Task SaveTranslation(string key, Language language, string value) 38 | { 39 | throw new NotImplementedException(); 40 | } 41 | 42 | public Task RemoveTranslation(string key) 43 | { 44 | throw new NotImplementedException(); 45 | } 46 | 47 | public Task> GetTranslationsCount() 48 | { 49 | throw new NotImplementedException(); 50 | } 51 | } -------------------------------------------------------------------------------- /translord.EntityFramework.Postgres/Migrations/20240316093826_InitialCreate.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Infrastructure; 4 | using Microsoft.EntityFrameworkCore.Migrations; 5 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 6 | using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; 7 | using translord.EntityFramework.Postgres; 8 | 9 | #nullable disable 10 | 11 | namespace translord.EntityFramework.Postgres.Migrations 12 | { 13 | [DbContext(typeof(TranslationsPostgresDbContext))] 14 | [Migration("20240316093826_InitialCreate")] 15 | partial class InitialCreate 16 | { 17 | /// 18 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 19 | { 20 | #pragma warning disable 612, 618 21 | modelBuilder 22 | .HasAnnotation("ProductVersion", "8.0.3") 23 | .HasAnnotation("Relational:MaxIdentifierLength", 63); 24 | 25 | NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); 26 | 27 | modelBuilder.Entity("translord.Models.Translation", b => 28 | { 29 | b.Property("Key") 30 | .HasColumnType("text"); 31 | 32 | b.Property("Language") 33 | .HasColumnType("integer"); 34 | 35 | b.Property("Value") 36 | .IsRequired() 37 | .HasColumnType("text"); 38 | 39 | b.HasKey("Key", "Language"); 40 | 41 | b.ToTable("Translations"); 42 | }); 43 | #pragma warning restore 612, 618 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /translord.Manager/Components/Translation/ImportTranslationsDialog.razor: -------------------------------------------------------------------------------- 1 | @using translord.Enums 2 | @inject ITranslator Translator 3 | @implements IDialogContentComponent> 4 | 5 | 6 | @foreach(var language in Translator.GetSupportedLanguages()) 7 | { 8 | 9 | @language.GetName() 10 | 17 | 18 | 19 | Upload @language.GetName() file 20 | 21 | @if (Content.TryGetValue(language, out var file)) 22 | { 23 |
24 | @file.Name - 25 | @($"{Decimal.Divide(@file.Size, 1024):N} KB") 26 |
27 | } 28 |
29 | } 30 |
31 | 32 | @code { 33 | [Parameter] public required Dictionary Content { get; set; } 34 | 35 | void OnCompleted(Language lang, IEnumerable files) 36 | { 37 | Content[lang] = files.First(); 38 | } 39 | } -------------------------------------------------------------------------------- /translord.EntityFramework.Postgres/translord.EntityFramework.Postgres.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | translord.EntityFramework.Postgres 8 | 0.1.0 9 | Mateusz Margos 10 | translord.EntityFramework.Postgres 11 | translord.EntityFramework.Postgres - package containing configuration of the translations store using EF Core with Postgres for translord. 12 | https://github.com/margosmat/translord/tree/main/translord.EntityFramework.Postgres 13 | MIT 14 | README.md 15 | icon.jpeg 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | all 26 | runtime; build; native; contentfiles; analyzers; buildtransitive 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /examples/WebApiWithUI/my-ui-app/src/app/i18n/index.ts: -------------------------------------------------------------------------------- 1 | import { ResourceKey, createInstance } from "i18next"; 2 | import { initReactI18next } from "react-i18next/initReactI18next"; 3 | import { getOptions } from "./settings"; 4 | import HttpBackend, { HttpBackendOptions } from "i18next-http-backend"; 5 | import axiosInstance from "../api/axiosInstance"; 6 | 7 | const initI18next = async (lng: string, ns: string) => { 8 | const i18nInstance = createInstance(); 9 | await i18nInstance 10 | .use(initReactI18next) 11 | .use(HttpBackend) 12 | .init({ 13 | backend: { 14 | request: async (options, url, data, callback) => { 15 | try { 16 | 17 | const result = await axiosInstance.get(`https:/localhost:7035/api/translations/${lng}`); 18 | console.log(result.data); 19 | callback(null, { 20 | data: result.data, 21 | status: 200, 22 | }); 23 | } catch (e) { 24 | console.error(e); 25 | callback(null, { 26 | data: {}, 27 | status: 500, 28 | }); 29 | } 30 | } 31 | }, 32 | ...getOptions(lng, ns), 33 | }); 34 | return i18nInstance; 35 | }; 36 | 37 | export async function useTranslation( 38 | lng: string, 39 | ns: string = "translations", 40 | options: { keyPrefix?: string } = {} 41 | ) { 42 | const i18nextInstance = await initI18next(lng, ns); 43 | return { 44 | t: i18nextInstance.getFixedT( 45 | lng, 46 | Array.isArray(ns) ? ns[0] : ns, 47 | options.keyPrefix 48 | ), 49 | i18n: i18nextInstance, 50 | }; 51 | } 52 | 53 | export const mapToi18nextSupportedLanguage = (l: string) => { 54 | if (l == 'en-gb') return 'en-GB' 55 | if (l == 'en-us') return 'en-US' 56 | if (l == 'pt-br') return 'pt-BR' 57 | if (l == 'pt-pt') return 'pt-PT' 58 | return l 59 | } 60 | -------------------------------------------------------------------------------- /translord/TranslordServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using translord.Core; 3 | 4 | namespace translord; 5 | 6 | public static class TranslordServiceCollectionExtensions 7 | { 8 | public static IServiceCollection AddTranslord(this IServiceCollection services, 9 | Action setupOptions) 10 | { 11 | var options = new TranslatorConfigurationOptions(); 12 | setupOptions(options); 13 | services.AddTransient(x => 14 | { 15 | var config = new TranslatorConfiguration(options, x.GetRequiredService(), 16 | x.GetService()); 17 | return config.CreateTranslator(); 18 | }); 19 | return services; 20 | } 21 | 22 | public static IServiceCollection AddTranslordCustomStore(this IServiceCollection services) 23 | where T : class, ITranslationsStore 24 | { 25 | services.AddTransient(); 26 | return services; 27 | } 28 | 29 | public static IServiceCollection AddTranslordCustomCache(this IServiceCollection services) 30 | where T : class, ITranslationsCache 31 | { 32 | services.AddTransient(); 33 | return services; 34 | } 35 | 36 | public static IServiceCollection AddTranslordCustomTranslator(this IServiceCollection services) 37 | where T : class, ILanguageTranslator 38 | { 39 | services.AddTransient(); 40 | return services; 41 | } 42 | 43 | public static IServiceCollection AddTranslordFileStore(this IServiceCollection services, 44 | Action setupOptions) 45 | { 46 | var options = new FileStoreOptions(); 47 | setupOptions(options); 48 | services.AddTransient(x => new FileStore(options, x.GetService())); 49 | return services; 50 | } 51 | } -------------------------------------------------------------------------------- /translord.Manager/Components/Account/IdentityRevalidatingAuthenticationStateProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Claims; 2 | using Microsoft.AspNetCore.Components.Authorization; 3 | using Microsoft.AspNetCore.Components.Server; 4 | using Microsoft.AspNetCore.Identity; 5 | using Microsoft.Extensions.Options; 6 | using translord.Manager.Data; 7 | 8 | namespace translord.Manager.Components.Account; 9 | 10 | // This is a server-side AuthenticationStateProvider that revalidates the security stamp for the connected user 11 | // every 30 minutes an interactive circuit is connected. 12 | internal sealed class IdentityRevalidatingAuthenticationStateProvider( 13 | ILoggerFactory loggerFactory, 14 | IServiceScopeFactory scopeFactory, 15 | IOptions options) 16 | : RevalidatingServerAuthenticationStateProvider(loggerFactory) 17 | { 18 | protected override TimeSpan RevalidationInterval => TimeSpan.FromMinutes(30); 19 | 20 | protected override async Task ValidateAuthenticationStateAsync( 21 | AuthenticationState authenticationState, CancellationToken cancellationToken) 22 | { 23 | // Get the user manager from a new scope to ensure it fetches fresh data 24 | await using var scope = scopeFactory.CreateAsyncScope(); 25 | var userManager = scope.ServiceProvider.GetRequiredService>(); 26 | return await ValidateSecurityStampAsync(userManager, authenticationState.User); 27 | } 28 | 29 | private async Task ValidateSecurityStampAsync(UserManager userManager, 30 | ClaimsPrincipal principal) 31 | { 32 | var user = await userManager.GetUserAsync(principal); 33 | if (user is null) 34 | { 35 | return false; 36 | } 37 | else if (!userManager.SupportsUserSecurityStamp) 38 | { 39 | return true; 40 | } 41 | else 42 | { 43 | var principalStamp = principal.FindFirstValue(options.Value.ClaimsIdentity.SecurityStampClaimType); 44 | var userStamp = await userManager.GetSecurityStampAsync(user); 45 | return principalStamp == userStamp; 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /translord.Manager/Components/Account/Pages/Manage/ResetAuthenticator.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/Manage/ResetAuthenticator" 2 | 3 | @using Microsoft.AspNetCore.Identity 4 | @using translord.Manager.Data 5 | 6 | @inject UserManager UserManager 7 | @inject SignInManager SignInManager 8 | @inject IdentityUserAccessor UserAccessor 9 | @inject IdentityRedirectManager RedirectManager 10 | @inject ILogger Logger 11 | 12 | Reset authenticator key 13 | 14 | 15 |

Reset authenticator key

16 | 26 |
27 |
28 | 29 | 30 | 31 |
32 | 33 | @code { 34 | [CascadingParameter] private HttpContext HttpContext { get; set; } = default!; 35 | 36 | private async Task OnSubmitAsync() 37 | { 38 | var user = await UserAccessor.GetRequiredUserAsync(HttpContext); 39 | await UserManager.SetTwoFactorEnabledAsync(user, false); 40 | await UserManager.ResetAuthenticatorKeyAsync(user); 41 | var userId = await UserManager.GetUserIdAsync(user); 42 | Logger.LogInformation("User with ID '{UserId}' has reset their authentication app key.", userId); 43 | 44 | await SignInManager.RefreshSignInAsync(user); 45 | 46 | RedirectManager.RedirectToWithStatus( 47 | "Account/Manage/EnableAuthenticator", 48 | "Your authenticator app key has been reset, you will need to configure your authenticator app using the new key.", 49 | HttpContext); 50 | } 51 | 52 | } -------------------------------------------------------------------------------- /translord.Manager/Components/Layout/MainLayout.razor.css: -------------------------------------------------------------------------------- 1 | .page { 2 | position: relative; 3 | display: flex; 4 | flex-direction: column; 5 | } 6 | 7 | main { 8 | flex: 1; 9 | } 10 | 11 | .sidebar { 12 | background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); 13 | } 14 | 15 | .top-row { 16 | background-color: #f7f7f7; 17 | border-bottom: 1px solid #d6d5d5; 18 | justify-content: flex-end; 19 | height: 3.5rem; 20 | display: flex; 21 | align-items: center; 22 | } 23 | 24 | .top-row ::deep a, .top-row ::deep .btn-link { 25 | white-space: nowrap; 26 | margin-left: 1.5rem; 27 | text-decoration: none; 28 | } 29 | 30 | .top-row ::deep a:hover, .top-row ::deep .btn-link:hover { 31 | text-decoration: underline; 32 | } 33 | 34 | .top-row ::deep a:first-child { 35 | overflow: hidden; 36 | text-overflow: ellipsis; 37 | } 38 | 39 | @media (max-width: 640.98px) { 40 | .top-row { 41 | justify-content: space-between; 42 | } 43 | 44 | .top-row ::deep a, .top-row ::deep .btn-link { 45 | margin-left: 0; 46 | } 47 | } 48 | 49 | @media (min-width: 641px) { 50 | .page { 51 | flex-direction: row; 52 | } 53 | 54 | .sidebar { 55 | width: 250px; 56 | height: 100vh; 57 | position: sticky; 58 | top: 0; 59 | } 60 | 61 | .top-row { 62 | position: sticky; 63 | top: 0; 64 | z-index: 1; 65 | } 66 | 67 | .top-row.auth ::deep a:first-child { 68 | flex: 1; 69 | text-align: right; 70 | width: 0; 71 | } 72 | 73 | .top-row, article { 74 | padding-left: 2rem !important; 75 | padding-right: 1.5rem !important; 76 | } 77 | } 78 | 79 | #blazor-error-ui { 80 | background: lightyellow; 81 | bottom: 0; 82 | box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); 83 | display: none; 84 | left: 0; 85 | padding: 0.6rem 1.25rem 0.7rem 1.25rem; 86 | position: fixed; 87 | width: 100%; 88 | z-index: 1000; 89 | } 90 | 91 | #blazor-error-ui .dismiss { 92 | cursor: pointer; 93 | position: absolute; 94 | right: 0.75rem; 95 | top: 0.5rem; 96 | } 97 | -------------------------------------------------------------------------------- /translord.Manager/Components/Account/Pages/ConfirmEmailChange.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/ConfirmEmailChange" 2 | 3 | @using System.Text 4 | @using Microsoft.AspNetCore.Identity 5 | @using Microsoft.AspNetCore.WebUtilities 6 | @using translord.Manager.Data 7 | 8 | @inject UserManager UserManager 9 | @inject SignInManager SignInManager 10 | @inject IdentityRedirectManager RedirectManager 11 | 12 | Confirm email change 13 | 14 |

Confirm email change

15 | 16 | 17 | 18 | @code { 19 | private string? message; 20 | 21 | [CascadingParameter] private HttpContext HttpContext { get; set; } = default!; 22 | 23 | [SupplyParameterFromQuery] private string? UserId { get; set; } 24 | 25 | [SupplyParameterFromQuery] private string? Email { get; set; } 26 | 27 | [SupplyParameterFromQuery] private string? Code { get; set; } 28 | 29 | protected override async Task OnInitializedAsync() 30 | { 31 | if (UserId is null || Email is null || Code is null) 32 | { 33 | RedirectManager.RedirectToWithStatus( 34 | "Account/Login", "Error: Invalid email change confirmation link.", HttpContext); 35 | } 36 | 37 | var user = await UserManager.FindByIdAsync(UserId); 38 | if (user is null) 39 | { 40 | message = "Unable to find user with Id '{userId}'"; 41 | return; 42 | } 43 | 44 | var code = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(Code)); 45 | var result = await UserManager.ChangeEmailAsync(user, Email, code); 46 | if (!result.Succeeded) 47 | { 48 | message = "Error changing email."; 49 | return; 50 | } 51 | 52 | // In our UI email and user name are one and the same, so when we update the email 53 | // we need to update the user name. 54 | var setUserNameResult = await UserManager.SetUserNameAsync(user, Email); 55 | if (!setUserNameResult.Succeeded) 56 | { 57 | message = "Error changing user name."; 58 | return; 59 | } 60 | 61 | await SignInManager.RefreshSignInAsync(user); 62 | message = "Thank you for confirming your email change."; 63 | } 64 | 65 | } -------------------------------------------------------------------------------- /translord.Manager/Components/Account/IdentityRedirectManager.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using Microsoft.AspNetCore.Components; 3 | 4 | namespace translord.Manager.Components.Account; 5 | 6 | internal sealed class IdentityRedirectManager(NavigationManager navigationManager) 7 | { 8 | public const string StatusCookieName = "Identity.StatusMessage"; 9 | 10 | private static readonly CookieBuilder StatusCookieBuilder = new() 11 | { 12 | SameSite = SameSiteMode.Strict, 13 | HttpOnly = true, 14 | IsEssential = true, 15 | MaxAge = TimeSpan.FromSeconds(5), 16 | }; 17 | 18 | [DoesNotReturn] 19 | public void RedirectTo(string? uri) 20 | { 21 | uri ??= ""; 22 | 23 | // Prevent open redirects. 24 | if (!Uri.IsWellFormedUriString(uri, UriKind.Relative)) 25 | { 26 | uri = navigationManager.ToBaseRelativePath(uri); 27 | } 28 | 29 | // During static rendering, NavigateTo throws a NavigationException which is handled by the framework as a redirect. 30 | // So as long as this is called from a statically rendered Identity component, the InvalidOperationException is never thrown. 31 | navigationManager.NavigateTo(uri); 32 | throw new InvalidOperationException( 33 | $"{nameof(IdentityRedirectManager)} can only be used during static rendering."); 34 | } 35 | 36 | [DoesNotReturn] 37 | public void RedirectTo(string uri, Dictionary queryParameters) 38 | { 39 | var uriWithoutQuery = navigationManager.ToAbsoluteUri(uri).GetLeftPart(UriPartial.Path); 40 | var newUri = navigationManager.GetUriWithQueryParameters(uriWithoutQuery, queryParameters); 41 | RedirectTo(newUri); 42 | } 43 | 44 | [DoesNotReturn] 45 | public void RedirectToWithStatus(string uri, string message, HttpContext context) 46 | { 47 | context.Response.Cookies.Append(StatusCookieName, message, StatusCookieBuilder.Build(context)); 48 | RedirectTo(uri); 49 | } 50 | 51 | private string CurrentPath => navigationManager.ToAbsoluteUri(navigationManager.Uri).GetLeftPart(UriPartial.Path); 52 | 53 | [DoesNotReturn] 54 | public void RedirectToCurrentPage() => RedirectTo(CurrentPath); 55 | 56 | [DoesNotReturn] 57 | public void RedirectToCurrentPageWithStatus(string message, HttpContext context) 58 | => RedirectToWithStatus(CurrentPath, message, context); 59 | } -------------------------------------------------------------------------------- /translord.Manager/Components/Account/Pages/Manage/Disable2fa.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/Manage/Disable2fa" 2 | 3 | @using Microsoft.AspNetCore.Identity 4 | @using translord.Manager.Data 5 | 6 | @inject UserManager UserManager 7 | @inject IdentityUserAccessor UserAccessor 8 | @inject IdentityRedirectManager RedirectManager 9 | @inject ILogger Logger 10 | 11 | Disable two-factor authentication (2FA) 12 | 13 | 14 |

Disable two-factor authentication (2FA)

15 | 16 | 25 | 26 |
27 |
28 | 29 | 30 | 31 |
32 | 33 | @code { 34 | private ApplicationUser user = default!; 35 | 36 | [CascadingParameter] private HttpContext HttpContext { get; set; } = default!; 37 | 38 | protected override async Task OnInitializedAsync() 39 | { 40 | user = await UserAccessor.GetRequiredUserAsync(HttpContext); 41 | 42 | if (HttpMethods.IsGet(HttpContext.Request.Method) && !await UserManager.GetTwoFactorEnabledAsync(user)) 43 | { 44 | throw new InvalidOperationException("Cannot disable 2FA for user as it's not currently enabled."); 45 | } 46 | } 47 | 48 | private async Task OnSubmitAsync() 49 | { 50 | var disable2faResult = await UserManager.SetTwoFactorEnabledAsync(user, false); 51 | if (!disable2faResult.Succeeded) 52 | { 53 | throw new InvalidOperationException("Unexpected error occurred disabling 2FA."); 54 | } 55 | 56 | var userId = await UserManager.GetUserIdAsync(user); 57 | Logger.LogInformation("User with ID '{UserId}' has disabled 2fa.", userId); 58 | RedirectManager.RedirectToWithStatus( 59 | "Account/Manage/TwoFactorAuthentication", 60 | "2fa has been disabled. You can reenable 2fa when you setup an authenticator app", 61 | HttpContext); 62 | } 63 | 64 | } -------------------------------------------------------------------------------- /translord.Manager/Components/Account/Pages/RegisterConfirmation.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/RegisterConfirmation" 2 | 3 | @using System.Text 4 | @using Microsoft.AspNetCore.Identity 5 | @using Microsoft.AspNetCore.WebUtilities 6 | @using translord.Manager.Data 7 | 8 | @inject UserManager UserManager 9 | @inject IEmailSender EmailSender 10 | @inject NavigationManager NavigationManager 11 | @inject IdentityRedirectManager RedirectManager 12 | 13 | Register confirmation 14 | 15 |

Register confirmation

16 | 17 | 18 | 19 | @if (emailConfirmationLink is not null) 20 | { 21 |

22 | This app does not currently have a real email sender registered, see these docs for how to configure a real email sender. 23 | Normally this would be emailed: Click here to confirm your account 24 |

25 | } 26 | else 27 | { 28 |

Please check your email to confirm your account.

29 | } 30 | 31 | @code { 32 | private string? emailConfirmationLink; 33 | private string? statusMessage; 34 | 35 | [CascadingParameter] private HttpContext HttpContext { get; set; } = default!; 36 | 37 | [SupplyParameterFromQuery] private string? Email { get; set; } 38 | 39 | [SupplyParameterFromQuery] private string? ReturnUrl { get; set; } 40 | 41 | protected override async Task OnInitializedAsync() 42 | { 43 | if (Email is null) 44 | { 45 | RedirectManager.RedirectTo(""); 46 | } 47 | 48 | var user = await UserManager.FindByEmailAsync(Email); 49 | if (user is null) 50 | { 51 | HttpContext.Response.StatusCode = StatusCodes.Status404NotFound; 52 | statusMessage = "Error finding user for unspecified email"; 53 | } 54 | else if (EmailSender is IdentityNoOpEmailSender) 55 | { 56 | // Once you add a real email sender, you should remove this code that lets you confirm the account 57 | var userId = await UserManager.GetUserIdAsync(user); 58 | var code = await UserManager.GenerateEmailConfirmationTokenAsync(user); 59 | code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); 60 | emailConfirmationLink = NavigationManager.GetUriWithQueryParameters( 61 | NavigationManager.ToAbsoluteUri("Account/ConfirmEmail").AbsoluteUri, 62 | new Dictionary { ["userId"] = userId, ["code"] = code, ["returnUrl"] = ReturnUrl }); 63 | } 64 | } 65 | 66 | } -------------------------------------------------------------------------------- /examples/WebApiWithUI/WebApi/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using System.Collections.Concurrent; 3 | using translord; 4 | using translord.Core; 5 | using translord.DeepL; 6 | using translord.Enums; 7 | using translord.Manager; 8 | using translord.Manager.Data; 9 | using translord.RedisCache; 10 | 11 | var builder = WebApplication.CreateBuilder(args); 12 | 13 | // Add services to the container. 14 | // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle 15 | builder.Services.AddEndpointsApiExplorer(); 16 | builder.Services.AddSwaggerGen(); 17 | builder.Services.AddSingleton(new ConcurrentDictionary()); 18 | 19 | List supportedLanguages = 20 | [ 21 | Language.EnglishBritish, Language.Polish, Language.German, Language.French, Language.Japanese, Language.Spanish, 22 | Language.Ukrainian, Language.Czech 23 | ]; 24 | builder.Services.AddTranslordRedisCache(x => 25 | { 26 | x.Server = "localhost"; 27 | x.Port = 6379; 28 | }) 29 | .AddTranslordFileStore(options => 30 | { 31 | options.TranslationsPath = Path.Combine(Directory.GetCurrentDirectory(), "translations"); 32 | }) 33 | .AddTranslordDeepLTranslator(options => { options.AuthKey = builder.Configuration["DeepLAuthKey"]; }) 34 | .AddTranslord(o => 35 | { 36 | o.SupportedLanguages = supportedLanguages; 37 | o.DefaultLanguage = Language.EnglishBritish; 38 | }); 39 | 40 | builder.Services.AddDbContext(options => 41 | options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection") ?? string.Empty, 42 | b => b.MigrationsAssembly("WebApi"))); 43 | builder.AddTranslordManager(); 44 | 45 | var cache = builder.Services.BuildServiceProvider().GetService(); 46 | if (cache is not null) 47 | { 48 | await cache.RemoveAll(supportedLanguages.Select(x => $"{x}").ToList()); 49 | } 50 | 51 | var app = builder.Build(); 52 | 53 | // Configure the HTTP request pipeline. 54 | if (app.Environment.IsDevelopment()) 55 | { 56 | app.UseSwagger(); 57 | app.UseSwaggerUI(); 58 | } 59 | 60 | app.UseHttpsRedirection(); 61 | 62 | app.MapGet("/api/translations/{languageIsoCode}", async (string languageIsoCode, ITranslator translator) => 63 | { 64 | var language = languageIsoCode.ToLower().FromIsoCode(); 65 | 66 | return await translator.GetAllTranslationsRawJson(language); 67 | }) 68 | .WithName("GetTranslations") 69 | .WithOpenApi(); 70 | 71 | app.MapGet("/api/supported-languages", 72 | (ITranslator translator) => translator.GetSupportedLanguages().Select(x => x.GetIsoCode())) 73 | .WithName("GetSupportedLanguages") 74 | .WithOpenApi(); 75 | 76 | app.UseTranslordManager(); 77 | 78 | app.Run(); -------------------------------------------------------------------------------- /translord.Manager/Components/Account/Pages/Manage/GenerateRecoveryCodes.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/Manage/GenerateRecoveryCodes" 2 | 3 | @using Microsoft.AspNetCore.Identity 4 | @using translord.Manager.Data 5 | 6 | @inject UserManager UserManager 7 | @inject IdentityUserAccessor UserAccessor 8 | @inject IdentityRedirectManager RedirectManager 9 | @inject ILogger Logger 10 | 11 | Generate two-factor authentication (2FA) recovery codes 12 | 13 | @if (recoveryCodes is not null) 14 | { 15 | 16 | } 17 | else 18 | { 19 |

Generate two-factor authentication (2FA) recovery codes

20 | 33 |
34 |
35 | 36 | 37 | 38 |
39 | } 40 | 41 | @code { 42 | private string? message; 43 | private ApplicationUser user = default!; 44 | private IEnumerable? recoveryCodes; 45 | 46 | [CascadingParameter] private HttpContext HttpContext { get; set; } = default!; 47 | 48 | protected override async Task OnInitializedAsync() 49 | { 50 | user = await UserAccessor.GetRequiredUserAsync(HttpContext); 51 | 52 | var isTwoFactorEnabled = await UserManager.GetTwoFactorEnabledAsync(user); 53 | if (!isTwoFactorEnabled) 54 | { 55 | throw new InvalidOperationException("Cannot generate recovery codes for user because they do not have 2FA enabled."); 56 | } 57 | } 58 | 59 | private async Task OnSubmitAsync() 60 | { 61 | var userId = await UserManager.GetUserIdAsync(user); 62 | recoveryCodes = await UserManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10); 63 | message = "You have generated new recovery codes."; 64 | 65 | Logger.LogInformation("User with ID '{UserId}' has generated new 2FA recovery codes.", userId); 66 | } 67 | 68 | } -------------------------------------------------------------------------------- /translord.Manager/Components/Account/Pages/ResendEmailConfirmation.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/ResendEmailConfirmation" 2 | 3 | @using System.ComponentModel.DataAnnotations 4 | @using System.Text 5 | @using System.Text.Encodings.Web 6 | @using Microsoft.AspNetCore.Identity 7 | @using Microsoft.AspNetCore.WebUtilities 8 | @using translord.Manager.Data 9 | 10 | @inject UserManager UserManager 11 | @inject IEmailSender EmailSender 12 | @inject NavigationManager NavigationManager 13 | @inject IdentityRedirectManager RedirectManager 14 | 15 | Resend email confirmation 16 | 17 |

Resend email confirmation

18 |

Enter your email.

19 |
20 | 21 |
22 |
23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
31 | 32 |
33 |
34 |
35 | 36 | @code { 37 | private string? message; 38 | 39 | [SupplyParameterFromForm] private InputModel Input { get; set; } = new(); 40 | 41 | private async Task OnValidSubmitAsync() 42 | { 43 | var user = await UserManager.FindByEmailAsync(Input.Email!); 44 | if (user is null) 45 | { 46 | message = "Verification email sent. Please check your email."; 47 | return; 48 | } 49 | 50 | var userId = await UserManager.GetUserIdAsync(user); 51 | var code = await UserManager.GenerateEmailConfirmationTokenAsync(user); 52 | code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); 53 | var callbackUrl = NavigationManager.GetUriWithQueryParameters( 54 | NavigationManager.ToAbsoluteUri("Account/ConfirmEmail").AbsoluteUri, 55 | new Dictionary { ["userId"] = userId, ["code"] = code }); 56 | await EmailSender.SendConfirmationLinkAsync(user, Input.Email, HtmlEncoder.Default.Encode(callbackUrl)); 57 | 58 | message = "Verification email sent. Please check your email."; 59 | } 60 | 61 | private sealed class InputModel 62 | { 63 | [Required] [EmailAddress] public string Email { get; set; } = ""; 64 | } 65 | 66 | } -------------------------------------------------------------------------------- /translord.Manager/Components/Account/Pages/ForgotPassword.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/ForgotPassword" 2 | 3 | @using System.ComponentModel.DataAnnotations 4 | @using System.Text 5 | @using System.Text.Encodings.Web 6 | @using Microsoft.AspNetCore.Identity 7 | @using Microsoft.AspNetCore.WebUtilities 8 | @using translord.Manager.Data 9 | 10 | @inject UserManager UserManager 11 | @inject IEmailSender EmailSender 12 | @inject NavigationManager NavigationManager 13 | @inject IdentityRedirectManager RedirectManager 14 | 15 | Forgot your password? 16 | 17 |

Forgot your password?

18 |

Enter your email.

19 |
20 |
21 |
22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
31 | 32 |
33 |
34 |
35 | 36 | @code { 37 | [SupplyParameterFromForm] private InputModel Input { get; set; } = new(); 38 | 39 | private async Task OnValidSubmitAsync() 40 | { 41 | var user = await UserManager.FindByEmailAsync(Input.Email); 42 | if (user is null || !(await UserManager.IsEmailConfirmedAsync(user))) 43 | { 44 | // Don't reveal that the user does not exist or is not confirmed 45 | RedirectManager.RedirectTo("Account/ForgotPasswordConfirmation"); 46 | } 47 | 48 | // For more information on how to enable account confirmation and password reset please 49 | // visit https://go.microsoft.com/fwlink/?LinkID=532713 50 | var code = await UserManager.GeneratePasswordResetTokenAsync(user); 51 | code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); 52 | var callbackUrl = NavigationManager.GetUriWithQueryParameters( 53 | NavigationManager.ToAbsoluteUri("Account/ResetPassword").AbsoluteUri, 54 | new Dictionary { ["code"] = code }); 55 | 56 | await EmailSender.SendPasswordResetLinkAsync(user, Input.Email, HtmlEncoder.Default.Encode(callbackUrl)); 57 | 58 | RedirectManager.RedirectTo("Account/ForgotPasswordConfirmation"); 59 | } 60 | 61 | private sealed class InputModel 62 | { 63 | [Required] [EmailAddress] public string Email { get; set; } = ""; 64 | } 65 | 66 | } -------------------------------------------------------------------------------- /translord.Manager/translord.Manager.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | aspnet-translord.Manager-371988E9-EA43-44C9-8B25-9B12C86D6A56 8 | translord.Manager 9 | Library 10 | translord.Manager 11 | 0.1.4 12 | Mateusz Margos 13 | translord.Manager 14 | translord.Manager - package containing TMS admin panel for translord. 15 | https://github.com/margosmat/translord/tree/main/translord.Manager 16 | MIT 17 | README.md 18 | icon.jpeg 19 | true 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | PreserveNewest 39 | PreserveNewest 40 | 41 | 42 | PreserveNewest 43 | PreserveNewest 44 | 45 | 46 | PreserveNewest 47 | PreserveNewest 48 | 49 | 50 | PreserveNewest 51 | PreserveNewest 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /translord.Manager/Components/Account/Pages/Manage/Index.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/Manage" 2 | 3 | @using System.ComponentModel.DataAnnotations 4 | @using Microsoft.AspNetCore.Identity 5 | @using translord.Manager.Data 6 | 7 | @inject UserManager UserManager 8 | @inject SignInManager SignInManager 9 | @inject IdentityUserAccessor UserAccessor 10 | @inject IdentityRedirectManager RedirectManager 11 | 12 | Profile 13 | 14 |

Profile

15 | 16 | 17 |
18 |
19 | 20 | 21 | 22 |
23 | 24 | 25 |
26 |
27 | 28 | 29 | 30 |
31 | 32 |
33 |
34 |
35 | 36 | @code { 37 | private ApplicationUser user = default!; 38 | private string? username; 39 | private string? phoneNumber; 40 | 41 | [CascadingParameter] private HttpContext HttpContext { get; set; } = default!; 42 | 43 | [SupplyParameterFromForm] private InputModel Input { get; set; } = new(); 44 | 45 | protected override async Task OnInitializedAsync() 46 | { 47 | user = await UserAccessor.GetRequiredUserAsync(HttpContext); 48 | username = await UserManager.GetUserNameAsync(user); 49 | phoneNumber = await UserManager.GetPhoneNumberAsync(user); 50 | 51 | Input.PhoneNumber ??= phoneNumber; 52 | } 53 | 54 | private async Task OnValidSubmitAsync() 55 | { 56 | if (Input.PhoneNumber != phoneNumber) 57 | { 58 | var setPhoneResult = await UserManager.SetPhoneNumberAsync(user, Input.PhoneNumber); 59 | if (!setPhoneResult.Succeeded) 60 | { 61 | RedirectManager.RedirectToCurrentPageWithStatus("Error: Failed to set phone number.", HttpContext); 62 | } 63 | } 64 | 65 | await SignInManager.RefreshSignInAsync(user); 66 | RedirectManager.RedirectToCurrentPageWithStatus("Your profile has been updated", HttpContext); 67 | } 68 | 69 | private sealed class InputModel 70 | { 71 | [Phone] 72 | [Display(Name = "Phone number")] 73 | public string? PhoneNumber { get; set; } 74 | } 75 | 76 | } -------------------------------------------------------------------------------- /translord.Manager/TranslordManagerExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Microsoft.AspNetCore.Components.Authorization; 3 | using Microsoft.AspNetCore.Identity; 4 | using Microsoft.Extensions.FileProviders; 5 | using Microsoft.FluentUI.AspNetCore.Components; 6 | using translord.Manager.Components; 7 | using translord.Manager.Components.Account; 8 | using translord.Manager.Data; 9 | 10 | namespace translord.Manager; 11 | 12 | public static class TranslordManagerExtensions 13 | { 14 | public static WebApplicationBuilder AddTranslordManager(this WebApplicationBuilder builder) 15 | { 16 | // Add services to the container. 17 | builder.Services.AddRazorComponents() 18 | .AddInteractiveServerComponents(); 19 | 20 | builder.Services.AddCascadingAuthenticationState(); 21 | builder.Services.AddScoped(); 22 | builder.Services.AddScoped(); 23 | builder.Services.AddScoped(); 24 | 25 | builder.Services.AddAuthentication(options => 26 | { 27 | options.DefaultScheme = IdentityConstants.ApplicationScheme; 28 | options.DefaultSignInScheme = IdentityConstants.ExternalScheme; 29 | }) 30 | .AddIdentityCookies(); 31 | 32 | builder.Services.AddDatabaseDeveloperPageExceptionFilter(); 33 | 34 | builder.Services.AddIdentityCore(options => options.SignIn.RequireConfirmedAccount = true) 35 | .AddEntityFrameworkStores() 36 | .AddSignInManager() 37 | .AddDefaultTokenProviders(); 38 | 39 | builder.Services.AddSingleton, IdentityNoOpEmailSender>(); 40 | builder.Services.AddHttpClient(); 41 | builder.Services.AddFluentUIComponents(); 42 | 43 | return builder; 44 | } 45 | 46 | public static WebApplication UseTranslordManager(this WebApplication app) 47 | { 48 | // Configure the HTTP request pipeline. 49 | if (app.Environment.IsDevelopment()) 50 | { 51 | app.UseMigrationsEndPoint(); 52 | } 53 | else 54 | { 55 | app.UseExceptionHandler("/Error", createScopeForErrors: true); 56 | // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. 57 | app.UseHsts(); 58 | } 59 | 60 | app.UseStaticFiles(); 61 | var location = Assembly.GetEntryAssembly()?.Location; 62 | var path = location?.Remove(location.LastIndexOf('/')); 63 | app.UseStaticFiles(new StaticFileOptions { 64 | FileProvider = new PhysicalFileProvider(path + "/wwwroot") 65 | }); 66 | app.UseAntiforgery(); 67 | 68 | app.MapRazorComponents() 69 | .AddInteractiveServerRenderMode(); 70 | 71 | // Add additional endpoints required by the Identity /Account Razor components. 72 | app.MapAdditionalIdentityEndpoints(); 73 | 74 | return app; 75 | } 76 | } -------------------------------------------------------------------------------- /translord.Manager/Components/Account/Pages/Manage/DeletePersonalData.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/Manage/DeletePersonalData" 2 | 3 | @using System.ComponentModel.DataAnnotations 4 | @using Microsoft.AspNetCore.Identity 5 | @using translord.Manager.Data 6 | 7 | @inject UserManager UserManager 8 | @inject SignInManager SignInManager 9 | @inject IdentityUserAccessor UserAccessor 10 | @inject IdentityRedirectManager RedirectManager 11 | @inject ILogger Logger 12 | 13 | Delete Personal Data 14 | 15 | 16 | 17 |

Delete Personal Data

18 | 19 | 24 | 25 |
26 | 27 | 28 | 29 | @if (requirePassword) 30 | { 31 |
32 | 33 | 34 | 35 |
36 | } 37 | 38 |
39 |
40 | 41 | @code { 42 | private string? message; 43 | private ApplicationUser user = default!; 44 | private bool requirePassword; 45 | 46 | [CascadingParameter] private HttpContext HttpContext { get; set; } = default!; 47 | 48 | [SupplyParameterFromForm] private InputModel Input { get; set; } = new(); 49 | 50 | protected override async Task OnInitializedAsync() 51 | { 52 | Input ??= new(); 53 | user = await UserAccessor.GetRequiredUserAsync(HttpContext); 54 | requirePassword = await UserManager.HasPasswordAsync(user); 55 | } 56 | 57 | private async Task OnValidSubmitAsync() 58 | { 59 | if (requirePassword && !await UserManager.CheckPasswordAsync(user, Input.Password)) 60 | { 61 | message = "Error: Incorrect password."; 62 | return; 63 | } 64 | 65 | var result = await UserManager.DeleteAsync(user); 66 | if (!result.Succeeded) 67 | { 68 | throw new InvalidOperationException("Unexpected error occurred deleting user."); 69 | } 70 | 71 | await SignInManager.SignOutAsync(); 72 | 73 | var userId = await UserManager.GetUserIdAsync(user); 74 | Logger.LogInformation("User with ID '{UserId}' deleted themselves.", userId); 75 | 76 | RedirectManager.RedirectToCurrentPage(); 77 | } 78 | 79 | private sealed class InputModel 80 | { 81 | [DataType(DataType.Password)] public string Password { get; set; } = ""; 82 | } 83 | 84 | } -------------------------------------------------------------------------------- /translord.Manager/Components/Account/Pages/LoginWithRecoveryCode.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/LoginWithRecoveryCode" 2 | 3 | @using System.ComponentModel.DataAnnotations 4 | @using Microsoft.AspNetCore.Identity 5 | @using translord.Manager.Data 6 | 7 | @inject SignInManager SignInManager 8 | @inject UserManager UserManager 9 | @inject IdentityRedirectManager RedirectManager 10 | @inject ILogger Logger 11 | 12 | Recovery code verification 13 | 14 |

Recovery code verification

15 |
16 | 17 |

18 | You have requested to log in with a recovery code. This login will not be remembered until you provide 19 | an authenticator app code at log in or disable 2FA and log in again. 20 |

21 |
22 |
23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
31 | 32 |
33 |
34 |
35 | 36 | @code { 37 | private string? message; 38 | private ApplicationUser user = default!; 39 | 40 | [SupplyParameterFromForm] private InputModel Input { get; set; } = new(); 41 | 42 | [SupplyParameterFromQuery] private string? ReturnUrl { get; set; } 43 | 44 | protected override async Task OnInitializedAsync() 45 | { 46 | // Ensure the user has gone through the username & password screen first 47 | user = await SignInManager.GetTwoFactorAuthenticationUserAsync() ?? 48 | throw new InvalidOperationException("Unable to load two-factor authentication user."); 49 | } 50 | 51 | private async Task OnValidSubmitAsync() 52 | { 53 | var recoveryCode = Input.RecoveryCode.Replace(" ", string.Empty); 54 | 55 | var result = await SignInManager.TwoFactorRecoveryCodeSignInAsync(recoveryCode); 56 | 57 | var userId = await UserManager.GetUserIdAsync(user); 58 | 59 | if (result.Succeeded) 60 | { 61 | Logger.LogInformation("User with ID '{UserId}' logged in with a recovery code.", userId); 62 | RedirectManager.RedirectTo(ReturnUrl); 63 | } 64 | else if (result.IsLockedOut) 65 | { 66 | Logger.LogWarning("User account locked out."); 67 | RedirectManager.RedirectTo("Account/Lockout"); 68 | } 69 | else 70 | { 71 | Logger.LogWarning("Invalid recovery code entered for user with ID '{UserId}' ", userId); 72 | message = "Error: Invalid recovery code entered."; 73 | } 74 | } 75 | 76 | private sealed class InputModel 77 | { 78 | [Required] 79 | [DataType(DataType.Text)] 80 | [Display(Name = "Recovery Code")] 81 | public string RecoveryCode { get; set; } = ""; 82 | } 83 | 84 | } -------------------------------------------------------------------------------- /translord.EntityFramework/EfStore.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using Microsoft.EntityFrameworkCore; 3 | using translord.EntityFramework.Data; 4 | using translord.Enums; 5 | using translord.Models; 6 | 7 | namespace translord.EntityFramework; 8 | 9 | internal class EfStore : ITranslationsStore 10 | { 11 | private readonly TranslationsDbContext _context; 12 | private readonly ITranslationsCache? _cache; 13 | 14 | TranslatorConfiguration? ITranslationsStore.Config { get; set; } 15 | 16 | public EfStore(TranslationsDbContext context, ITranslationsCache? cache) 17 | { 18 | _context = context; 19 | _cache = cache; 20 | } 21 | 22 | public async Task GetSerializedTranslations(Language language) 23 | { 24 | if (_cache is not null) 25 | { 26 | var json = await _cache.Get($"{language}"); 27 | if (!string.IsNullOrEmpty(json)) return json; 28 | } 29 | 30 | var translations = await _context.Translations.Where(x => x.Language == language).ToDictionaryAsync(x => x.Key, x => x.Value); 31 | var serializedJson = JsonSerializer.Serialize(translations); 32 | if (_cache is not null) await _cache.Add($"{language}", serializedJson); 33 | return serializedJson; 34 | } 35 | 36 | public async Task> GetAllKeys() 37 | { 38 | var allKeys = await _context.Translations.Select(x => x.Key).Distinct().ToListAsync(); 39 | return allKeys; 40 | } 41 | 42 | public async Task SaveTranslation(string key, Language language, string value) 43 | { 44 | var existingTranslation = await _context.Translations.FirstOrDefaultAsync(x => x.Language == language && x.Key == key); 45 | if (existingTranslation is not null) 46 | { 47 | existingTranslation.Value = value; 48 | _context.Translations.Update(existingTranslation); 49 | } 50 | else 51 | { 52 | _context.Translations.Add(new Translation { Key = key, Value = value, Language = language }); 53 | } 54 | 55 | await _context.SaveChangesAsync(); 56 | 57 | if (_cache is not null) await _cache.Remove($"{language}"); 58 | } 59 | 60 | public async Task RemoveTranslation(string key) 61 | { 62 | var translationsToRemove = await _context.Translations.Where(x => x.Key == key).ToListAsync(); 63 | if (translationsToRemove.Any()) 64 | { 65 | _context.Translations.RemoveRange(translationsToRemove); 66 | await _context.SaveChangesAsync(); 67 | foreach (var lang in translationsToRemove.Select(x => x.Language).Distinct()) 68 | { 69 | if (_cache is not null) await _cache.Remove($"{lang}"); 70 | } 71 | } 72 | } 73 | 74 | public async Task> GetTranslationsCount() 75 | { 76 | var translationsCount = new List<(Language lang, int count)>(); 77 | var configSupportedLanguages = ((ITranslationsStore)this).Config?.SupportedLanguages; 78 | if (configSupportedLanguages == null) return translationsCount; 79 | foreach (var lang in configSupportedLanguages) 80 | { 81 | var count = await _context.Translations.CountAsync(x => x.Language == lang); 82 | translationsCount.Add((lang, count)); 83 | } 84 | 85 | return translationsCount; 86 | } 87 | } -------------------------------------------------------------------------------- /translord.Manager/Components/Translation/TranslationDialog.razor: -------------------------------------------------------------------------------- 1 | @using translord.Enums 2 | @using translord.Manager.Models 3 | @inject ITranslator Translator 4 | @implements IDialogContentComponent 5 | 6 | 7 |
8 | @if (IsAddingNewLabel) 9 | { 10 | 15 | 16 | } 17 | else 18 | { 19 | @Content.Key 20 | } 21 |
22 | Translations: 23 |
24 | @foreach (var lang in Translator.GetSupportedLanguages()) 25 | { 26 | 27 | 32 | 33 | @if (Translator.IsTranslationSupported) 34 | { 35 | 39 | 40 | } 41 | 42 | } 43 |
44 |
45 | 46 | @code { 47 | [Parameter] public GroupedTranslations Content { get; set; } = default!; 48 | 49 | [CascadingParameter] 50 | public FluentDialog? Dialog { get; set; } 51 | 52 | bool IsAddingNewLabel { get; set; } 53 | Language? DefaultLang { get; set; } 54 | 55 | protected override async Task OnInitializedAsync() 56 | { 57 | await base.OnInitializedAsync(); 58 | IsAddingNewLabel = string.IsNullOrEmpty(Content.Key); 59 | DefaultLang = Translator.GetDefaultLanguage(); 60 | ValidateDialog(); 61 | } 62 | 63 | private void ValidateDialog() 64 | { 65 | if (DefaultLang.HasValue) 66 | { 67 | Dialog?.TogglePrimaryActionButton(!string.IsNullOrEmpty(Content.Translations.FirstOrDefault(x => x.Language == DefaultLang)?.Value) && !string.IsNullOrEmpty(Content.Key)); 68 | } 69 | else 70 | { 71 | Dialog?.TogglePrimaryActionButton(Content.Translations.Any(x => string.IsNullOrEmpty(x.Value)) && !string.IsNullOrEmpty(Content.Key)); 72 | } 73 | } 74 | 75 | private async Task TranslateLabel(Language to) 76 | { 77 | var fromLang = DefaultLang ?? 78 | Content.Translations.First(x => x.Language != to && !string.IsNullOrEmpty(x.Value)).Language; 79 | var translation = await Translator.Translate(Content.Translations.Find(x => x.Language == fromLang)!.Value, fromLang, to); 80 | Content.Translations.Find(x => x.Language == to)!.Value = translation; 81 | } 82 | } -------------------------------------------------------------------------------- /examples/ConsoleApp/Program.cs: -------------------------------------------------------------------------------- 1 | // See https://aka.ms/new-console-template for more information 2 | 3 | using translord; 4 | using translord.Core; 5 | using translord.DeepL; 6 | using translord.Enums; 7 | 8 | List supportedLanguages = [Language.EnglishBritish, Language.Polish, Language.German, Language.Estonian]; 9 | var path = Path.Combine(Directory.GetCurrentDirectory(), "translations"); 10 | var deeplTranslator = new DeepLTranslator(new AddTranslordDeepLTranslatorOptions { AuthKey = "your-auth-key" }); 11 | var translator = 12 | new TranslatorConfiguration( 13 | new TranslatorConfigurationOptions { SupportedLanguages = supportedLanguages, DefaultLanguage = Language.Polish }, 14 | new FileStore(new FileStoreOptions { TranslationsPath = path }, null), 15 | deeplTranslator).CreateTranslator(); 16 | 17 | var translations = await translator.GetAllTranslations(); 18 | var groupedTranslations = translations.GroupBy(x => x.Language).ToList(); 19 | var defaultLanguageTranslations = translations.Where(x => x.Language == translator.GetDefaultLanguage().Value).ToList(); 20 | 21 | foreach (var translation in groupedTranslations) 22 | { 23 | var missingTranslations = defaultLanguageTranslations.Select(x => x.Key).Where(x => string.IsNullOrEmpty(translation.FirstOrDefault(y => y.Key == x).Value)).ToList(); 24 | if (missingTranslations.Any()) 25 | { 26 | foreach (var missingTranslation in missingTranslations) 27 | { 28 | var translationValue = await translator.Translate(defaultLanguageTranslations.First(x => x.Key == missingTranslation).Value, translator.GetDefaultLanguage().Value, translation.Key); 29 | await translator.SaveTranslation(missingTranslation, translation.Key, translationValue); 30 | } 31 | } 32 | } 33 | 34 | while (true) 35 | { 36 | Console.WriteLine($"Enter language code to translate: ({supportedLanguages.Select(x => x.GetIsoCode()).Aggregate((x, y) => $"{x}, {y}")})"); 37 | var key = Console.ReadLine(); 38 | if (!supportedLanguages.Select(x => x.GetIsoCode()).Contains(key.ToLower())) 39 | { 40 | Console.WriteLine("Invalid language code."); 41 | continue; 42 | } 43 | 44 | var translationP1 = await translator.GetTranslation("message.paragraph1", supportedLanguages.First(x => x.GetIsoCode() == key.ToLower())); 45 | Console.WriteLine(translationP1); 46 | 47 | if (translator.GetDefaultLanguage().HasValue && translator.GetDefaultLanguage().Value.GetIsoCode() == key.ToLower()) 48 | { 49 | var translationP2 = await translator.GetTranslation("message.paragraph2", supportedLanguages.First(x => x.GetIsoCode() == key.ToLower())); 50 | Console.WriteLine(translationP2); 51 | } 52 | 53 | var translationP3 = await translator.GetTranslation("message.paragraph3", supportedLanguages.First(x => x.GetIsoCode() == key.ToLower())); 54 | Console.WriteLine(translationP3); 55 | 56 | var translationP4 = await translator.GetTranslation("message.paragraph4", supportedLanguages.First(x => x.GetIsoCode() == key.ToLower())); 57 | Console.WriteLine(translationP4); 58 | 59 | var translationP5 = await translator.GetTranslation("message.paragraph5", supportedLanguages.First(x => x.GetIsoCode() == key.ToLower())); 60 | Console.WriteLine(translationP5); 61 | 62 | var translationP6 = await translator.GetTranslation("message.paragraph6", supportedLanguages.First(x => x.GetIsoCode() == key.ToLower())); 63 | Console.WriteLine(translationP6); 64 | 65 | var translationP7 = await translator.GetTranslation("message.paragraph7", supportedLanguages.First(x => x.GetIsoCode() == key.ToLower())); 66 | Console.WriteLine(translationP7); 67 | } 68 | -------------------------------------------------------------------------------- /examples/WebApiWithUI/my-ui-app/src/app/[lng]/page.tsx: -------------------------------------------------------------------------------- 1 | import { mapToi18nextSupportedLanguage, useTranslation } from "../i18n"; 2 | import axiosInstance from "../api/axiosInstance"; 3 | import Link from "next/link"; 4 | import Image from "next/image"; 5 | import Icon from "../images/icon.jpeg"; 6 | 7 | type HomeProps = { 8 | params: { 9 | lng: string; 10 | }; 11 | }; 12 | 13 | export default async function Home({ params: { lng } }: HomeProps) { 14 | let supportedLanguages: string[] = []; 15 | try { 16 | const response = await axiosInstance.get( 17 | "https:/localhost:7035/api/supported-languages" 18 | ); 19 | supportedLanguages = response.data.map(mapToi18nextSupportedLanguage); 20 | } catch (error) { 21 | console.error(error); 22 | } 23 | const { t } = await useTranslation(lng); 24 | 25 | return ( 26 |
27 |
28 |
29 | 39 |
    40 | {supportedLanguages 41 | .filter((l) => lng !== l) 42 | .map((l, index) => { 43 | return ( 44 |
  • 45 | 49 | {l} 50 | 51 |
  • 52 | ); 53 | })} 54 |
55 |
56 | translord Logo 63 |
64 | 65 |
66 | {t("message.paragraph1")} 😊 67 |
68 | {t("message.paragraph2")} 🏝️ 69 |
70 | {t("message.paragraph3")} 🤖 71 |
72 | {t("message.paragraph4")} 📦 73 |
74 | {t("message.paragraph5")} 🫡 75 |
76 | {t("message.paragraph6")} ⭐️ 77 |
78 | 79 |
80 |
81 | ); 82 | } 83 | -------------------------------------------------------------------------------- /translord.Manager/Components/Account/Pages/Manage/SetPassword.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/Manage/SetPassword" 2 | 3 | @using System.ComponentModel.DataAnnotations 4 | @using Microsoft.AspNetCore.Identity 5 | @using translord.Manager.Data 6 | 7 | @inject UserManager UserManager 8 | @inject SignInManager SignInManager 9 | @inject IdentityUserAccessor UserAccessor 10 | @inject IdentityRedirectManager RedirectManager 11 | 12 | Set password 13 | 14 |

Set your password

15 | 16 |

17 | You do not have a local username/password for this site. Add a local 18 | account so you can log in without an external login. 19 |

20 |
21 |
22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 |
30 |
31 | 32 | 33 | 34 |
35 | 36 |
37 |
38 |
39 | 40 | @code { 41 | private string? message; 42 | private ApplicationUser user = default!; 43 | 44 | [CascadingParameter] private HttpContext HttpContext { get; set; } = default!; 45 | 46 | [SupplyParameterFromForm] private InputModel Input { get; set; } = new(); 47 | 48 | protected override async Task OnInitializedAsync() 49 | { 50 | user = await UserAccessor.GetRequiredUserAsync(HttpContext); 51 | 52 | var hasPassword = await UserManager.HasPasswordAsync(user); 53 | if (hasPassword) 54 | { 55 | RedirectManager.RedirectTo("Account/Manage/ChangePassword"); 56 | } 57 | } 58 | 59 | private async Task OnValidSubmitAsync() 60 | { 61 | var addPasswordResult = await UserManager.AddPasswordAsync(user, Input.NewPassword!); 62 | if (!addPasswordResult.Succeeded) 63 | { 64 | message = $"Error: {string.Join(",", addPasswordResult.Errors.Select(error => error.Description))}"; 65 | return; 66 | } 67 | 68 | await SignInManager.RefreshSignInAsync(user); 69 | RedirectManager.RedirectToCurrentPageWithStatus("Your password has been set.", HttpContext); 70 | } 71 | 72 | private sealed class InputModel 73 | { 74 | [Required] 75 | [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] 76 | [DataType(DataType.Password)] 77 | [Display(Name = "New password")] 78 | public string? NewPassword { get; set; } 79 | 80 | [DataType(DataType.Password)] 81 | [Display(Name = "Confirm new password")] 82 | [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")] 83 | public string? ConfirmPassword { get; set; } 84 | } 85 | 86 | } -------------------------------------------------------------------------------- /translord.Manager/Components/Layout/NavMenu.razor: -------------------------------------------------------------------------------- 1 | @implements IDisposable 2 | @inject IConfiguration Configuration 3 | 4 | @inject NavigationManager NavigationManager 5 | 6 | 11 | 12 | 13 | 14 | 70 | 71 | @code { 72 | private string? currentUrl; 73 | 74 | protected override void OnInitialized() 75 | { 76 | currentUrl = NavigationManager.ToBaseRelativePath(NavigationManager.Uri); 77 | NavigationManager.LocationChanged += OnLocationChanged; 78 | } 79 | 80 | private void OnLocationChanged(object? sender, LocationChangedEventArgs e) 81 | { 82 | currentUrl = NavigationManager.ToBaseRelativePath(e.Location); 83 | StateHasChanged(); 84 | } 85 | 86 | public void Dispose() 87 | { 88 | NavigationManager.LocationChanged -= OnLocationChanged; 89 | } 90 | 91 | } -------------------------------------------------------------------------------- /translord.Manager/Components/Account/Pages/LoginWith2fa.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/LoginWith2fa" 2 | 3 | @using System.ComponentModel.DataAnnotations 4 | @using Microsoft.AspNetCore.Identity 5 | @using translord.Manager.Data 6 | 7 | @inject SignInManager SignInManager 8 | @inject UserManager UserManager 9 | @inject IdentityRedirectManager RedirectManager 10 | @inject ILogger Logger 11 | 12 | Two-factor authentication 13 | 14 |

Two-factor authentication

15 |
16 | 17 |

Your login is protected with an authenticator app. Enter your authenticator code below.

18 |
19 |
20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 |
30 |
31 | 35 |
36 |
37 | 38 |
39 |
40 |
41 |
42 |

43 | Don't have access to your authenticator device? You can 44 | log in with a recovery code. 45 |

46 | 47 | @code { 48 | private string? message; 49 | private ApplicationUser user = default!; 50 | 51 | [SupplyParameterFromForm] private InputModel Input { get; set; } = new(); 52 | 53 | [SupplyParameterFromQuery] private string? ReturnUrl { get; set; } 54 | 55 | [SupplyParameterFromQuery] private bool RememberMe { get; set; } 56 | 57 | protected override async Task OnInitializedAsync() 58 | { 59 | // Ensure the user has gone through the username & password screen first 60 | user = await SignInManager.GetTwoFactorAuthenticationUserAsync() ?? 61 | throw new InvalidOperationException("Unable to load two-factor authentication user."); 62 | } 63 | 64 | private async Task OnValidSubmitAsync() 65 | { 66 | var authenticatorCode = Input.TwoFactorCode!.Replace(" ", string.Empty).Replace("-", string.Empty); 67 | var result = await SignInManager.TwoFactorAuthenticatorSignInAsync(authenticatorCode, RememberMe, Input.RememberMachine); 68 | var userId = await UserManager.GetUserIdAsync(user); 69 | 70 | if (result.Succeeded) 71 | { 72 | Logger.LogInformation("User with ID '{UserId}' logged in with 2fa.", userId); 73 | RedirectManager.RedirectTo(ReturnUrl); 74 | } 75 | else if (result.IsLockedOut) 76 | { 77 | Logger.LogWarning("User with ID '{UserId}' account locked out.", userId); 78 | RedirectManager.RedirectTo("Account/Lockout"); 79 | } 80 | else 81 | { 82 | Logger.LogWarning("Invalid authenticator code entered for user with ID '{UserId}'.", userId); 83 | message = "Error: Invalid authenticator code."; 84 | } 85 | } 86 | 87 | private sealed class InputModel 88 | { 89 | [Required] 90 | [StringLength(7, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] 91 | [DataType(DataType.Text)] 92 | [Display(Name = "Authenticator code")] 93 | public string? TwoFactorCode { get; set; } 94 | 95 | [Display(Name = "Remember this machine")] 96 | public bool RememberMachine { get; set; } 97 | } 98 | 99 | } -------------------------------------------------------------------------------- /translord/README.md: -------------------------------------------------------------------------------- 1 | # translord 2 | 3 | translord - simple TMS to get your translations up and running in no time. 4 | 5 | 🚧 Project still in the early stages of development 6 | 7 | What this tool aims to achieve? To be a central place in your project that handles all things related to your translations: 8 | - storing 9 | - translating 10 | - delivering 11 | - management 12 | - revision 13 | 14 | ## Packages structure 15 | - translord 16 | - The core library 17 | - [translord.DeepL](https://github.com/margosmat/translord/tree/main/translord.DeepL) 18 | - Library containing [DeepL API](https://www.deepl.com/pro-api?cta=header-pro-api) configuration for texts translation in translord. 19 | - [translord.EntityFramework](https://github.com/margosmat/translord/tree/main/translord.EntityFramework) 20 | - Library containing configuration data that uses EntityFramework as its database abstraction. 21 | - [translord.EntityFramework.Postgres](https://github.com/margosmat/translord/tree/main/translord.EntityFramework.Postgres) 22 | - Library extending the `translord.EntityFramework` library with Postgres configuration. 23 | - [translord.Manager](https://github.com/margosmat/translord/tree/main/translord.Manager) 24 | - Library containing the TMS admin panel allowing for translations editing/management/translation. 25 | - [translord.RedisCache](https://github.com/margosmat/translord/tree/main/translord.RedisCache) 26 | - Library containing configuration for Redis as the cache for translord. 27 | 28 | ## Configuration examples 29 | 30 | ### WebApp with FileStore 31 | ```c# 32 | builder.Services.AddTranslordFileStore(options => 33 | { 34 | options.TranslationsPath = Path.Combine(Directory.GetCurrentDirectory(), "translations"); 35 | }); 36 | builder.Services.AddTranslord(o => 37 | { 38 | List supportedLanguages = [Language.English, Language.Polish, Language.German]; 39 | o.SupportedLanguages = supportedLanguages; 40 | o.IsCachingEnabled = true; 41 | }); 42 | ``` 43 | 44 | ### WebApp with PostgresStore and TMS panel 45 | ```c# 46 | builder.Services.AddTranslordPostgresStore(options => 47 | options.ConnectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? string.Empty); 48 | builder.Services.AddTranslord(o => 49 | { 50 | List supportedLanguages = [Language.English, Language.Polish, Language.German]; 51 | o.SupportedLanguages = supportedLanguages; 52 | o.IsCachingEnabled = true; 53 | }); 54 | builder.AddTranslordManager(); 55 | ``` 56 | 57 | ### Console app with FileStore 58 | ```c# 59 | List supportedLanguages = [ Language.English, Language.Polish ]; 60 | var path = Path.Combine(Directory.GetCurrentDirectory(), "translations"); 61 | var deeplTranslator = new DeepLTranslator(new AddTranslordDeepLTranslatorOptions { AuthKey = "your-auth-key" }); 62 | var translator = 63 | new TranslatorConfiguration( 64 | new TranslatorConfigurationOptions { SupportedLanguages = supportedLanguages, DefaultLanguage = Language.English }, 65 | new FileStore(new FileStoreOptions { TranslationsPath = path }, null), 66 | deeplTranslator).CreateTranslator(); 67 | 68 | var label = await translator.GetTranslation("label.test", Language.Polish); 69 | var translations = await translator.GetAllTranslations(Language.English); 70 | ``` 71 | 72 | ### Custom implementations 73 | You can configure your own custom implementations for the **store**, **cache** or **translator** in translord. Just implement specific interface and be sure to register it in DI: 74 | ```c# 75 | builder.Services.AddTranslordCustomStore(); 76 | builder.Services.AddTranslordCustomCache(); 77 | builder.Services.AddTranslordCustomTranslator(); 78 | ``` 79 | 80 | ## Import 81 | For now, you can import your translations in one specific way. You need one `.json` file per language, with root object containing string key-value pairs of your translations. In the future there could be more import schemas added, please add an issue if you need support for specific import schema. Example of json that can be imported now: 82 | ```json 83 | { 84 | "label.add": "add", 85 | "label.delete": "delete", 86 | ... 87 | } 88 | ``` 89 | 90 | See [GitHub](https://github.com/margosmat/translord) for more information and examples. -------------------------------------------------------------------------------- /translord.Manager/Components/Account/Pages/Manage/TwoFactorAuthentication.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/Manage/TwoFactorAuthentication" 2 | 3 | @using Microsoft.AspNetCore.Http.Features 4 | @using Microsoft.AspNetCore.Identity 5 | @using translord.Manager.Data 6 | 7 | @inject UserManager UserManager 8 | @inject SignInManager SignInManager 9 | @inject IdentityUserAccessor UserAccessor 10 | @inject IdentityRedirectManager RedirectManager 11 | 12 | Two-factor authentication (2FA) 13 | 14 | 15 |

Two-factor authentication (2FA)

16 | @if (canTrack) 17 | { 18 | if (is2faEnabled) 19 | { 20 | if (recoveryCodesLeft == 0) 21 | { 22 |
23 | You have no recovery codes left. 24 |

You must generate a new set of recovery codes before you can log in with a recovery code.

25 |
26 | } 27 | else if (recoveryCodesLeft == 1) 28 | { 29 |
30 | You have 1 recovery code left. 31 |

You can generate a new set of recovery codes.

32 |
33 | } 34 | else if (recoveryCodesLeft <= 3) 35 | { 36 |
37 | You have @recoveryCodesLeft recovery codes left. 38 |

You should generate a new set of recovery codes.

39 |
40 | } 41 | 42 | if (isMachineRemembered) 43 | { 44 |
45 | 46 | 47 | 48 | } 49 | 50 | Disable 2FA 51 | Reset recovery codes 52 | } 53 | 54 |

Authenticator app

55 | @if (!hasAuthenticator) 56 | { 57 | Add authenticator app 58 | } 59 | else 60 | { 61 | Set up authenticator app 62 | Reset authenticator app 63 | } 64 | } 65 | else 66 | { 67 |
68 | Privacy and cookie policy have not been accepted. 69 |

You must accept the policy before you can enable two factor authentication.

70 |
71 | } 72 | 73 | @code { 74 | private bool canTrack; 75 | private bool hasAuthenticator; 76 | private int recoveryCodesLeft; 77 | private bool is2faEnabled; 78 | private bool isMachineRemembered; 79 | 80 | [CascadingParameter] private HttpContext HttpContext { get; set; } = default!; 81 | 82 | protected override async Task OnInitializedAsync() 83 | { 84 | var user = await UserAccessor.GetRequiredUserAsync(HttpContext); 85 | canTrack = HttpContext.Features.Get()?.CanTrack ?? true; 86 | hasAuthenticator = await UserManager.GetAuthenticatorKeyAsync(user) is not null; 87 | is2faEnabled = await UserManager.GetTwoFactorEnabledAsync(user); 88 | isMachineRemembered = await SignInManager.IsTwoFactorClientRememberedAsync(user); 89 | recoveryCodesLeft = await UserManager.CountRecoveryCodesAsync(user); 90 | } 91 | 92 | private async Task OnSubmitForgetBrowserAsync() 93 | { 94 | await SignInManager.ForgetTwoFactorClientAsync(); 95 | 96 | RedirectManager.RedirectToCurrentPageWithStatus( 97 | "The current browser has been forgotten. When you login again from this browser you will be prompted for your 2fa code.", 98 | HttpContext); 99 | } 100 | 101 | } -------------------------------------------------------------------------------- /translord.Manager/Components/Account/Pages/ResetPassword.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/ResetPassword" 2 | 3 | @using System.ComponentModel.DataAnnotations 4 | @using System.Text 5 | @using Microsoft.AspNetCore.Identity 6 | @using Microsoft.AspNetCore.WebUtilities 7 | @using translord.Manager.Data 8 | 9 | @inject IdentityRedirectManager RedirectManager 10 | @inject UserManager UserManager 11 | 12 | Reset password 13 | 14 |

Reset password

15 |

Reset your password.

16 |
17 |
18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 |
30 |
31 | 32 | 33 | 34 |
35 |
36 | 37 | 38 | 39 |
40 | 41 |
42 |
43 |
44 | 45 | @code { 46 | private IEnumerable? identityErrors; 47 | 48 | [SupplyParameterFromForm] private InputModel Input { get; set; } = new(); 49 | 50 | [SupplyParameterFromQuery] private string? Code { get; set; } 51 | 52 | private string? Message => identityErrors is null ? null : $"Error: {string.Join(", ", identityErrors.Select(error => error.Description))}"; 53 | 54 | protected override void OnInitialized() 55 | { 56 | if (Code is null) 57 | { 58 | RedirectManager.RedirectTo("Account/InvalidPasswordReset"); 59 | } 60 | 61 | Input.Code = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(Code)); 62 | } 63 | 64 | private async Task OnValidSubmitAsync() 65 | { 66 | var user = await UserManager.FindByEmailAsync(Input.Email); 67 | if (user is null) 68 | { 69 | // Don't reveal that the user does not exist 70 | RedirectManager.RedirectTo("Account/ResetPasswordConfirmation"); 71 | } 72 | 73 | var result = await UserManager.ResetPasswordAsync(user, Input.Code, Input.Password); 74 | if (result.Succeeded) 75 | { 76 | RedirectManager.RedirectTo("Account/ResetPasswordConfirmation"); 77 | } 78 | 79 | identityErrors = result.Errors; 80 | } 81 | 82 | private sealed class InputModel 83 | { 84 | [Required] [EmailAddress] public string Email { get; set; } = ""; 85 | 86 | [Required] 87 | [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] 88 | [DataType(DataType.Password)] 89 | public string Password { get; set; } = ""; 90 | 91 | [DataType(DataType.Password)] 92 | [Display(Name = "Confirm password")] 93 | [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] 94 | public string ConfirmPassword { get; set; } = ""; 95 | 96 | [Required] public string Code { get; set; } = ""; 97 | } 98 | 99 | } -------------------------------------------------------------------------------- /translord.Manager/wwwroot/app.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 3 | } 4 | 5 | a, .btn-link { 6 | color: #006bb7; 7 | } 8 | 9 | .btn-primary { 10 | color: #fff; 11 | background-color: #1b6ec2; 12 | border-color: #1861ac; 13 | } 14 | 15 | main { 16 | width: calc(100% - 250px); 17 | overflow: hidden; 18 | } 19 | 20 | .btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus { 21 | box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb; 22 | } 23 | 24 | .content { 25 | padding-top: 1.1rem; 26 | } 27 | 28 | h1:focus { 29 | outline: none; 30 | } 31 | 32 | .valid.modified:not([type=checkbox]) { 33 | outline: 1px solid #26b050; 34 | } 35 | 36 | .invalid { 37 | outline: 1px solid #e50000; 38 | } 39 | 40 | .validation-message { 41 | color: #e50000; 42 | } 43 | 44 | .blazor-error-boundary { 45 | background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; 46 | padding: 1rem 1rem 1rem 3.7rem; 47 | color: white; 48 | } 49 | 50 | .blazor-error-boundary::after { 51 | content: "An error has occurred." 52 | } 53 | 54 | .darker-border-checkbox.form-check-input { 55 | border-color: #929292; 56 | } 57 | 58 | .translations-page { 59 | .grid { 60 | overflow: auto; 61 | height: 80vh; 62 | 63 | fluent-data-grid-cell[grid-column="1"] { 64 | z-index: 1; 65 | position: sticky; 66 | left: 0; 67 | background-color: white; 68 | } 69 | 70 | fluent-data-grid-cell[grid-column="2"] { 71 | z-index: 1; 72 | position: sticky; 73 | left: 200px; 74 | background-color: white; 75 | } 76 | } 77 | 78 | .grid table { 79 | min-width: 100%; 80 | } 81 | 82 | /* Sticky header while scrolling */ 83 | thead { 84 | position: sticky; 85 | top: 0; 86 | z-index: 1; 87 | } 88 | 89 | /* For virtualized grids, it's essential that all rows have the same known height */ 90 | tr { 91 | height: 30px; 92 | border-bottom: 0.5px solid black; 93 | } 94 | 95 | tbody td { 96 | white-space: nowrap; 97 | overflow: hidden; 98 | max-width: 0; 99 | text-overflow: ellipsis; 100 | border-right: 0.5px solid black; 101 | } 102 | 103 | tbody td:first-of-type { 104 | border-left: 0.5px solid black; 105 | } 106 | 107 | tbody td:last-of-type { 108 | position: sticky; 109 | } 110 | } 111 | 112 | .translations-dialog { 113 | .translation-wrapper label { 114 | width: 100px; 115 | } 116 | } 117 | 118 | @media (max-width: 640px) { 119 | main { 120 | width: 100%; 121 | } 122 | } -------------------------------------------------------------------------------- /translord.Manager/Components/Account/Pages/Manage/ChangePassword.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/Manage/ChangePassword" 2 | 3 | @using System.ComponentModel.DataAnnotations 4 | @using Microsoft.AspNetCore.Identity 5 | @using translord.Manager.Data 6 | 7 | @inject UserManager UserManager 8 | @inject SignInManager SignInManager 9 | @inject IdentityUserAccessor UserAccessor 10 | @inject IdentityRedirectManager RedirectManager 11 | @inject ILogger Logger 12 | 13 | Change password 14 | 15 |

Change password

16 | 17 |
18 |
19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 |
27 |
28 | 29 | 30 | 31 |
32 |
33 | 34 | 35 | 36 |
37 | 38 |
39 |
40 |
41 | 42 | @code { 43 | private string? message; 44 | private ApplicationUser user = default!; 45 | private bool hasPassword; 46 | 47 | [CascadingParameter] private HttpContext HttpContext { get; set; } = default!; 48 | 49 | [SupplyParameterFromForm] private InputModel Input { get; set; } = new(); 50 | 51 | protected override async Task OnInitializedAsync() 52 | { 53 | user = await UserAccessor.GetRequiredUserAsync(HttpContext); 54 | hasPassword = await UserManager.HasPasswordAsync(user); 55 | if (!hasPassword) 56 | { 57 | RedirectManager.RedirectTo("Account/Manage/SetPassword"); 58 | } 59 | } 60 | 61 | private async Task OnValidSubmitAsync() 62 | { 63 | var changePasswordResult = await UserManager.ChangePasswordAsync(user, Input.OldPassword, Input.NewPassword); 64 | if (!changePasswordResult.Succeeded) 65 | { 66 | message = $"Error: {string.Join(",", changePasswordResult.Errors.Select(error => error.Description))}"; 67 | return; 68 | } 69 | 70 | await SignInManager.RefreshSignInAsync(user); 71 | Logger.LogInformation("User changed their password successfully."); 72 | 73 | RedirectManager.RedirectToCurrentPageWithStatus("Your password has been changed", HttpContext); 74 | } 75 | 76 | private sealed class InputModel 77 | { 78 | [Required] 79 | [DataType(DataType.Password)] 80 | [Display(Name = "Current password")] 81 | public string OldPassword { get; set; } = ""; 82 | 83 | [Required] 84 | [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] 85 | [DataType(DataType.Password)] 86 | [Display(Name = "New password")] 87 | public string NewPassword { get; set; } = ""; 88 | 89 | [DataType(DataType.Password)] 90 | [Display(Name = "Confirm new password")] 91 | [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")] 92 | public string ConfirmPassword { get; set; } = ""; 93 | } 94 | 95 | } -------------------------------------------------------------------------------- /translord.Manager/Components/Account/Pages/Login.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/Login" 2 | 3 | @using System.ComponentModel.DataAnnotations 4 | @using Microsoft.AspNetCore.Authentication 5 | @using Microsoft.AspNetCore.Identity 6 | @using translord.Manager.Data 7 | 8 | @inject SignInManager SignInManager 9 | @inject ILogger Logger 10 | @inject NavigationManager NavigationManager 11 | @inject IdentityRedirectManager RedirectManager 12 | 13 | Log in 14 | 15 |

Log in

16 |
17 |
18 |
19 | 20 | 21 | 22 |

Use a local account to log in.

23 |
24 | 25 |
26 | 27 | 28 | 29 |
30 |
31 | 32 | 33 | 34 |
35 |
36 | 40 |
41 |
42 | 43 |
44 | 55 |
56 |
57 |
58 |
59 |
60 |

Use another service to log in.

61 |
62 | 63 |
64 |
65 |
66 | 67 | @code { 68 | private string? errorMessage; 69 | 70 | [CascadingParameter] private HttpContext HttpContext { get; set; } = default!; 71 | 72 | [SupplyParameterFromForm] private InputModel Input { get; set; } = new(); 73 | 74 | [SupplyParameterFromQuery] private string? ReturnUrl { get; set; } 75 | 76 | protected override async Task OnInitializedAsync() 77 | { 78 | if (HttpMethods.IsGet(HttpContext.Request.Method)) 79 | { 80 | // Clear the existing external cookie to ensure a clean login process 81 | await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); 82 | } 83 | } 84 | 85 | public async Task LoginUser() 86 | { 87 | // This doesn't count login failures towards account lockout 88 | // To enable password failures to trigger account lockout, set lockoutOnFailure: true 89 | var result = await SignInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: false); 90 | if (result.Succeeded) 91 | { 92 | Logger.LogInformation("User logged in."); 93 | RedirectManager.RedirectTo(ReturnUrl); 94 | } 95 | else if (result.RequiresTwoFactor) 96 | { 97 | RedirectManager.RedirectTo( 98 | "Account/LoginWith2fa", 99 | new() { ["returnUrl"] = ReturnUrl, ["rememberMe"] = Input.RememberMe }); 100 | } 101 | else if (result.IsLockedOut) 102 | { 103 | Logger.LogWarning("User account locked out."); 104 | RedirectManager.RedirectTo("Account/Lockout"); 105 | } 106 | else 107 | { 108 | errorMessage = "Error: Invalid login attempt."; 109 | } 110 | } 111 | 112 | private sealed class InputModel 113 | { 114 | [Required] [EmailAddress] public string Email { get; set; } = ""; 115 | 116 | [Required] 117 | [DataType(DataType.Password)] 118 | public string Password { get; set; } = ""; 119 | 120 | [Display(Name = "Remember me?")] public bool RememberMe { get; set; } 121 | } 122 | 123 | } -------------------------------------------------------------------------------- /translord.Manager/Components/Account/Pages/Manage/Email.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/Manage/Email" 2 | 3 | @using System.ComponentModel.DataAnnotations 4 | @using System.Text 5 | @using System.Text.Encodings.Web 6 | @using Microsoft.AspNetCore.Identity 7 | @using Microsoft.AspNetCore.WebUtilities 8 | @using translord.Manager.Data 9 | 10 | @inject UserManager UserManager 11 | @inject IEmailSender EmailSender 12 | @inject IdentityUserAccessor UserAccessor 13 | @inject NavigationManager NavigationManager 14 | 15 | Manage email 16 | 17 |

Manage email

18 | 19 | 20 |
21 |
22 |
23 | 24 | 25 | 26 | 27 | 28 | @if (isEmailConfirmed) 29 | { 30 |
31 | 32 |
33 | 34 |
35 | 36 |
37 | } 38 | else 39 | { 40 |
41 | 42 | 43 | 44 |
45 | } 46 |
47 | 48 | 49 | 50 |
51 | 52 |
53 |
54 |
55 | 56 | @code { 57 | private string? message; 58 | private ApplicationUser user = default!; 59 | private string? email; 60 | private bool isEmailConfirmed; 61 | 62 | [CascadingParameter] private HttpContext HttpContext { get; set; } = default!; 63 | 64 | [SupplyParameterFromForm(FormName = "change-email")] 65 | private InputModel Input { get; set; } = new(); 66 | 67 | protected override async Task OnInitializedAsync() 68 | { 69 | user = await UserAccessor.GetRequiredUserAsync(HttpContext); 70 | email = await UserManager.GetEmailAsync(user); 71 | isEmailConfirmed = await UserManager.IsEmailConfirmedAsync(user); 72 | 73 | Input.NewEmail ??= email; 74 | } 75 | 76 | private async Task OnValidSubmitAsync() 77 | { 78 | if (Input.NewEmail is null || Input.NewEmail == email) 79 | { 80 | message = "Your email is unchanged."; 81 | return; 82 | } 83 | 84 | var userId = await UserManager.GetUserIdAsync(user); 85 | var code = await UserManager.GenerateChangeEmailTokenAsync(user, Input.NewEmail); 86 | code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); 87 | var callbackUrl = NavigationManager.GetUriWithQueryParameters( 88 | NavigationManager.ToAbsoluteUri("Account/ConfirmEmailChange").AbsoluteUri, 89 | new Dictionary { ["userId"] = userId, ["email"] = Input.NewEmail, ["code"] = code }); 90 | 91 | await EmailSender.SendConfirmationLinkAsync(user, Input.NewEmail, HtmlEncoder.Default.Encode(callbackUrl)); 92 | 93 | message = "Confirmation link to change email sent. Please check your email."; 94 | } 95 | 96 | private async Task OnSendEmailVerificationAsync() 97 | { 98 | if (email is null) 99 | { 100 | return; 101 | } 102 | 103 | var userId = await UserManager.GetUserIdAsync(user); 104 | var code = await UserManager.GenerateEmailConfirmationTokenAsync(user); 105 | code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); 106 | var callbackUrl = NavigationManager.GetUriWithQueryParameters( 107 | NavigationManager.ToAbsoluteUri("Account/ConfirmEmail").AbsoluteUri, 108 | new Dictionary { ["userId"] = userId, ["code"] = code }); 109 | 110 | await EmailSender.SendConfirmationLinkAsync(user, email, HtmlEncoder.Default.Encode(callbackUrl)); 111 | 112 | message = "Verification email sent. Please check your email."; 113 | } 114 | 115 | private sealed class InputModel 116 | { 117 | [Required] 118 | [EmailAddress] 119 | [Display(Name = "New email")] 120 | public string? NewEmail { get; set; } 121 | } 122 | 123 | } -------------------------------------------------------------------------------- /translord.Manager/Components/Account/IdentityComponentsEndpointRouteBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Claims; 2 | using System.Text.Json; 3 | using Microsoft.AspNetCore.Authentication; 4 | using Microsoft.AspNetCore.Components.Authorization; 5 | using Microsoft.AspNetCore.Http.Extensions; 6 | using Microsoft.AspNetCore.Identity; 7 | using Microsoft.AspNetCore.Mvc; 8 | using Microsoft.Extensions.Primitives; 9 | using translord.Manager.Components.Account.Pages; 10 | using translord.Manager.Components.Account.Pages.Manage; 11 | using translord.Manager.Data; 12 | 13 | namespace Microsoft.AspNetCore.Routing; 14 | 15 | internal static class IdentityComponentsEndpointRouteBuilderExtensions 16 | { 17 | // These endpoints are required by the Identity Razor components defined in the /Components/Account/Pages directory of this project. 18 | public static IEndpointConventionBuilder MapAdditionalIdentityEndpoints(this IEndpointRouteBuilder endpoints) 19 | { 20 | ArgumentNullException.ThrowIfNull(endpoints); 21 | 22 | var accountGroup = endpoints.MapGroup("/Account"); 23 | 24 | accountGroup.MapPost("/PerformExternalLogin", ( 25 | HttpContext context, 26 | [FromServices] SignInManager signInManager, 27 | [FromForm] string provider, 28 | [FromForm] string returnUrl) => 29 | { 30 | IEnumerable> query = 31 | [ 32 | new("ReturnUrl", returnUrl), 33 | new("Action", ExternalLogin.LoginCallbackAction) 34 | ]; 35 | 36 | var redirectUrl = UriHelper.BuildRelative( 37 | context.Request.PathBase, 38 | "/Account/ExternalLogin", 39 | QueryString.Create(query)); 40 | 41 | var properties = signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl); 42 | return TypedResults.Challenge(properties, [provider]); 43 | }); 44 | 45 | accountGroup.MapPost("/Logout", async ( 46 | ClaimsPrincipal user, 47 | SignInManager signInManager, 48 | [FromForm] string returnUrl) => 49 | { 50 | await signInManager.SignOutAsync(); 51 | return TypedResults.LocalRedirect($"~/{returnUrl}"); 52 | }); 53 | 54 | var manageGroup = accountGroup.MapGroup("/Manage").RequireAuthorization(); 55 | 56 | manageGroup.MapPost("/LinkExternalLogin", async ( 57 | HttpContext context, 58 | [FromServices] SignInManager signInManager, 59 | [FromForm] string provider) => 60 | { 61 | // Clear the existing external cookie to ensure a clean login process 62 | await context.SignOutAsync(IdentityConstants.ExternalScheme); 63 | 64 | var redirectUrl = UriHelper.BuildRelative( 65 | context.Request.PathBase, 66 | "/Account/Manage/ExternalLogins", 67 | QueryString.Create("Action", ExternalLogins.LinkLoginCallbackAction)); 68 | 69 | var properties = signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl, 70 | signInManager.UserManager.GetUserId(context.User)); 71 | return TypedResults.Challenge(properties, [provider]); 72 | }); 73 | 74 | var loggerFactory = endpoints.ServiceProvider.GetRequiredService(); 75 | var downloadLogger = loggerFactory.CreateLogger("DownloadPersonalData"); 76 | 77 | manageGroup.MapPost("/DownloadPersonalData", async ( 78 | HttpContext context, 79 | [FromServices] UserManager userManager, 80 | [FromServices] AuthenticationStateProvider authenticationStateProvider) => 81 | { 82 | var user = await userManager.GetUserAsync(context.User); 83 | if (user is null) 84 | { 85 | return Results.NotFound($"Unable to load user with ID '{userManager.GetUserId(context.User)}'."); 86 | } 87 | 88 | var userId = await userManager.GetUserIdAsync(user); 89 | downloadLogger.LogInformation("User with ID '{UserId}' asked for their personal data.", userId); 90 | 91 | // Only include personal data for download 92 | var personalData = new Dictionary(); 93 | var personalDataProps = typeof(ApplicationUser).GetProperties().Where( 94 | prop => Attribute.IsDefined(prop, typeof(PersonalDataAttribute))); 95 | foreach (var p in personalDataProps) 96 | { 97 | personalData.Add(p.Name, p.GetValue(user)?.ToString() ?? "null"); 98 | } 99 | 100 | var logins = await userManager.GetLoginsAsync(user); 101 | foreach (var l in logins) 102 | { 103 | personalData.Add($"{l.LoginProvider} external login provider key", l.ProviderKey); 104 | } 105 | 106 | personalData.Add("Authenticator Key", (await userManager.GetAuthenticatorKeyAsync(user))!); 107 | var fileBytes = JsonSerializer.SerializeToUtf8Bytes(personalData); 108 | 109 | context.Response.Headers.TryAdd("Content-Disposition", "attachment; filename=PersonalData.json"); 110 | return TypedResults.File(fileBytes, contentType: "application/json", fileDownloadName: "PersonalData.json"); 111 | }); 112 | 113 | return accountGroup; 114 | } 115 | } -------------------------------------------------------------------------------- /translord/Core/Translator.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using translord.Enums; 3 | using translord.Models; 4 | 5 | namespace translord.Core; 6 | 7 | internal sealed class Translator( 8 | TranslatorConfiguration config, 9 | ITranslationsStore translationsStore, 10 | ILanguageTranslator? languageTranslator) : ITranslator 11 | { 12 | private TranslatorConfiguration Config { get; } = config; 13 | private ITranslationsStore TranslationsStore { get; } = translationsStore; 14 | public bool IsTranslationSupported { get; } = languageTranslator is not null; 15 | 16 | public async Task GetTranslation(string key, Language language) 17 | { 18 | try 19 | { 20 | var json = await TranslationsStore.GetSerializedTranslations(language); 21 | var deserializedJson = JsonSerializer.Deserialize(json); 22 | if (deserializedJson.TryGetProperty(key, out var value)) 23 | { 24 | return value.GetString() ?? ""; 25 | } 26 | } 27 | catch (Exception e) 28 | { 29 | Console.WriteLine(e); 30 | throw; 31 | } 32 | 33 | return string.Empty; 34 | } 35 | 36 | public async Task> GetAllTranslations(Language? language) 37 | { 38 | List translations; 39 | try 40 | { 41 | var allKeys = await TranslationsStore.GetAllKeys(); 42 | if (language.HasValue) 43 | { 44 | translations = await GetSingleLanguageTranslations(allKeys, language.Value); 45 | } 46 | else 47 | { 48 | var allLanguagesTranslations = new List(); 49 | foreach (var supportedLanguage in Config.SupportedLanguages) 50 | { 51 | var langTranslations = await GetSingleLanguageTranslations(allKeys, supportedLanguage); 52 | allLanguagesTranslations.AddRange(langTranslations); 53 | } 54 | 55 | translations = allLanguagesTranslations.ToList(); 56 | } 57 | } 58 | catch (Exception e) 59 | { 60 | Console.WriteLine(e); 61 | throw; 62 | } 63 | 64 | return translations; 65 | } 66 | 67 | private async Task> GetSingleLanguageTranslations(List allKeys, Language language) 68 | { 69 | var json = await TranslationsStore.GetSerializedTranslations(language); 70 | var jsonElements = new List(); 71 | 72 | if (!json.Equals(String.Empty)) 73 | { 74 | var deserializedJson = JsonSerializer.Deserialize(json); 75 | jsonElements = deserializedJson.EnumerateObject().ToList(); 76 | } 77 | 78 | return allKeys.Select(x => 79 | { 80 | var matchingElement = jsonElements.Find(y => y.Name.Equals(x)); 81 | var value = matchingElement.Value.ValueKind == JsonValueKind.Undefined 82 | ? String.Empty 83 | : matchingElement.Value.GetString(); 84 | return new Translation 85 | { 86 | Language = language, 87 | Key = x, 88 | Value = value ?? string.Empty 89 | }; 90 | }).ToList(); 91 | } 92 | 93 | public async Task GetAllTranslationsRawJson(Language language) 94 | { 95 | try 96 | { 97 | return await TranslationsStore.GetSerializedTranslations(language); 98 | } 99 | catch (Exception e) 100 | { 101 | Console.WriteLine(e); 102 | throw; 103 | } 104 | } 105 | 106 | public List GetSupportedLanguages() 107 | { 108 | return Config.SupportedLanguages.ToList(); 109 | } 110 | 111 | public Language? GetDefaultLanguage() 112 | { 113 | return Config.DefaultLanguage; 114 | } 115 | 116 | public async Task SaveTranslation(string key, Language language, string value) 117 | { 118 | await TranslationsStore.SaveTranslation(key, language, value); 119 | } 120 | 121 | public async Task RemoveTranslation(string key) 122 | { 123 | await TranslationsStore.RemoveTranslation(key); 124 | } 125 | 126 | public async Task> GetTranslationsCount() 127 | { 128 | return await TranslationsStore.GetTranslationsCount(); 129 | } 130 | 131 | public async Task Translate(string text, Language from, Language to) 132 | { 133 | return await languageTranslator!.Translate(text, from, to); 134 | } 135 | 136 | public async Task> Translate(string text, Language from, List to) 137 | { 138 | return await languageTranslator!.Translate(text, from, to); 139 | } 140 | 141 | public async Task ImportTranslations(JsonDocument json, Language language) 142 | { 143 | var translations = json.RootElement 144 | .EnumerateObject() 145 | .Select(p => new Translation 146 | { 147 | Key = p.Name, 148 | Value = p.Value.GetString() ?? string.Empty, 149 | Language = language 150 | }).Where(x => !string.IsNullOrEmpty(x.Value)).ToList(); 151 | foreach (var translation in translations) 152 | { 153 | await TranslationsStore.SaveTranslation(translation.Key, translation.Language, translation.Value); 154 | } 155 | } 156 | } -------------------------------------------------------------------------------- /translord.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{EF1DD318-5400-4DA8-8D68-BAD15B855322}" 4 | EndProject 5 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "translord", "translord\translord.csproj", "{620117CD-D3D2-419D-BE8D-C545438A4A99}" 6 | EndProject 7 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "examples", "examples", "{6DD0CFFB-AFB5-41C6-B6BA-B0C753B5BA96}" 8 | EndProject 9 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleApp", "examples\ConsoleApp\ConsoleApp.csproj", "{13BDFA77-4820-4E80-9FFD-3D3529726335}" 10 | EndProject 11 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "translord.EntityFramework", "translord.EntityFramework\translord.EntityFramework.csproj", "{96A50C03-B4F3-4A54-B16E-CC91992D90FB}" 12 | EndProject 13 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "translord.EntityFramework.Postgres", "translord.EntityFramework.Postgres\translord.EntityFramework.Postgres.csproj", "{907F28A0-AE24-4618-94D1-4299B0FFEA14}" 14 | EndProject 15 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "translord.Manager", "translord.Manager\translord.Manager.csproj", "{09D7F0A2-AEEE-49A4-907B-6E00CC01C893}" 16 | EndProject 17 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "translord.RedisCache", "translord.RedisCache\translord.RedisCache.csproj", "{D5D64505-A357-4779-878B-C9364A87D4E8}" 18 | EndProject 19 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "translord.DeepL", "translord.DeepL\translord.DeepL.csproj", "{86785580-9599-4388-9E03-B48BAECA2A2E}" 20 | EndProject 21 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApi", "examples\WebApiWithUI\WebApi\WebApi.csproj", "{F7AB1F8B-714D-4ECB-AAF2-44575DBE4D66}" 22 | EndProject 23 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "WebApiWithUI", "WebApiWithUI", "{926302C4-AD35-43AC-8F3B-23C508634E16}" 24 | EndProject 25 | Global 26 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 27 | Debug|Any CPU = Debug|Any CPU 28 | Release|Any CPU = Release|Any CPU 29 | EndGlobalSection 30 | GlobalSection(NestedProjects) = preSolution 31 | {620117CD-D3D2-419D-BE8D-C545438A4A99} = {EF1DD318-5400-4DA8-8D68-BAD15B855322} 32 | {13BDFA77-4820-4E80-9FFD-3D3529726335} = {6DD0CFFB-AFB5-41C6-B6BA-B0C753B5BA96} 33 | {96A50C03-B4F3-4A54-B16E-CC91992D90FB} = {EF1DD318-5400-4DA8-8D68-BAD15B855322} 34 | {907F28A0-AE24-4618-94D1-4299B0FFEA14} = {EF1DD318-5400-4DA8-8D68-BAD15B855322} 35 | {09D7F0A2-AEEE-49A4-907B-6E00CC01C893} = {EF1DD318-5400-4DA8-8D68-BAD15B855322} 36 | {D5D64505-A357-4779-878B-C9364A87D4E8} = {EF1DD318-5400-4DA8-8D68-BAD15B855322} 37 | {86785580-9599-4388-9E03-B48BAECA2A2E} = {EF1DD318-5400-4DA8-8D68-BAD15B855322} 38 | {926302C4-AD35-43AC-8F3B-23C508634E16} = {6DD0CFFB-AFB5-41C6-B6BA-B0C753B5BA96} 39 | {F7AB1F8B-714D-4ECB-AAF2-44575DBE4D66} = {926302C4-AD35-43AC-8F3B-23C508634E16} 40 | EndGlobalSection 41 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 42 | {620117CD-D3D2-419D-BE8D-C545438A4A99}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 43 | {620117CD-D3D2-419D-BE8D-C545438A4A99}.Debug|Any CPU.Build.0 = Debug|Any CPU 44 | {620117CD-D3D2-419D-BE8D-C545438A4A99}.Release|Any CPU.ActiveCfg = Release|Any CPU 45 | {620117CD-D3D2-419D-BE8D-C545438A4A99}.Release|Any CPU.Build.0 = Release|Any CPU 46 | {13BDFA77-4820-4E80-9FFD-3D3529726335}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 47 | {13BDFA77-4820-4E80-9FFD-3D3529726335}.Debug|Any CPU.Build.0 = Debug|Any CPU 48 | {13BDFA77-4820-4E80-9FFD-3D3529726335}.Release|Any CPU.ActiveCfg = Release|Any CPU 49 | {13BDFA77-4820-4E80-9FFD-3D3529726335}.Release|Any CPU.Build.0 = Release|Any CPU 50 | {96A50C03-B4F3-4A54-B16E-CC91992D90FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 51 | {96A50C03-B4F3-4A54-B16E-CC91992D90FB}.Debug|Any CPU.Build.0 = Debug|Any CPU 52 | {96A50C03-B4F3-4A54-B16E-CC91992D90FB}.Release|Any CPU.ActiveCfg = Release|Any CPU 53 | {96A50C03-B4F3-4A54-B16E-CC91992D90FB}.Release|Any CPU.Build.0 = Release|Any CPU 54 | {907F28A0-AE24-4618-94D1-4299B0FFEA14}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 55 | {907F28A0-AE24-4618-94D1-4299B0FFEA14}.Debug|Any CPU.Build.0 = Debug|Any CPU 56 | {907F28A0-AE24-4618-94D1-4299B0FFEA14}.Release|Any CPU.ActiveCfg = Release|Any CPU 57 | {907F28A0-AE24-4618-94D1-4299B0FFEA14}.Release|Any CPU.Build.0 = Release|Any CPU 58 | {09D7F0A2-AEEE-49A4-907B-6E00CC01C893}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 59 | {09D7F0A2-AEEE-49A4-907B-6E00CC01C893}.Debug|Any CPU.Build.0 = Debug|Any CPU 60 | {09D7F0A2-AEEE-49A4-907B-6E00CC01C893}.Release|Any CPU.ActiveCfg = Release|Any CPU 61 | {09D7F0A2-AEEE-49A4-907B-6E00CC01C893}.Release|Any CPU.Build.0 = Release|Any CPU 62 | {D5D64505-A357-4779-878B-C9364A87D4E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 63 | {D5D64505-A357-4779-878B-C9364A87D4E8}.Debug|Any CPU.Build.0 = Debug|Any CPU 64 | {D5D64505-A357-4779-878B-C9364A87D4E8}.Release|Any CPU.ActiveCfg = Release|Any CPU 65 | {D5D64505-A357-4779-878B-C9364A87D4E8}.Release|Any CPU.Build.0 = Release|Any CPU 66 | {86785580-9599-4388-9E03-B48BAECA2A2E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 67 | {86785580-9599-4388-9E03-B48BAECA2A2E}.Debug|Any CPU.Build.0 = Debug|Any CPU 68 | {86785580-9599-4388-9E03-B48BAECA2A2E}.Release|Any CPU.ActiveCfg = Release|Any CPU 69 | {86785580-9599-4388-9E03-B48BAECA2A2E}.Release|Any CPU.Build.0 = Release|Any CPU 70 | {F7AB1F8B-714D-4ECB-AAF2-44575DBE4D66}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 71 | {F7AB1F8B-714D-4ECB-AAF2-44575DBE4D66}.Debug|Any CPU.Build.0 = Debug|Any CPU 72 | {F7AB1F8B-714D-4ECB-AAF2-44575DBE4D66}.Release|Any CPU.ActiveCfg = Release|Any CPU 73 | {F7AB1F8B-714D-4ECB-AAF2-44575DBE4D66}.Release|Any CPU.Build.0 = Release|Any CPU 74 | EndGlobalSection 75 | EndGlobal 76 | -------------------------------------------------------------------------------- /translord/Core/FileStore.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Nodes; 3 | using translord.Enums; 4 | 5 | namespace translord.Core; 6 | 7 | public record FileStoreOptions 8 | { 9 | public string TranslationsPath { get; set; } 10 | } 11 | 12 | public sealed class FileStore(FileStoreOptions options, ITranslationsCache? cache) : ITranslationsStore 13 | { 14 | TranslatorConfiguration? ITranslationsStore.Config { get; set; } 15 | 16 | private string TranslationsPath { get; } = options.TranslationsPath; 17 | 18 | public async Task GetSerializedTranslations(Language language) 19 | { 20 | if (cache is not null) 21 | { 22 | var json = await cache.Get($"{language}"); 23 | if (!string.IsNullOrEmpty(json)) return json; 24 | } 25 | 26 | var filePath = $@"{TranslationsPath}/translations.{language.GetIsoCode()}.json"; 27 | if (!File.Exists(filePath)) return string.Empty; 28 | var serializedJson = await File.ReadAllTextAsync(filePath); 29 | if (cache is not null) await cache.Add($"{language}", serializedJson); 30 | return serializedJson; 31 | } 32 | 33 | public async Task> GetAllKeys() 34 | { 35 | var keys = new List(); 36 | if (((ITranslationsStore)this).Config?.DefaultLanguage.HasValue ?? false) 37 | { 38 | var defaultLanguageKeys = await GetKeysFromLanguageFile(((ITranslationsStore)this).Config.DefaultLanguage.Value); 39 | keys.AddRange(defaultLanguageKeys); 40 | } 41 | else 42 | { 43 | var configSupportedLanguages = ((ITranslationsStore)this).Config?.SupportedLanguages; 44 | if (configSupportedLanguages == null) return keys; 45 | foreach (var lang in configSupportedLanguages) 46 | { 47 | var names = await GetKeysFromLanguageFile(lang); 48 | keys.AddRange(names); 49 | } 50 | } 51 | 52 | return keys.Distinct().ToList(); 53 | } 54 | 55 | private async Task> GetKeysFromLanguageFile(Language lang) 56 | { 57 | var filePath = $@"{TranslationsPath}/translations.{lang.GetIsoCode()}.json"; 58 | if (!File.Exists(filePath)) return Enumerable.Empty(); 59 | 60 | await using var fs = new FileStream(filePath, FileMode.Open); 61 | using var document = await JsonDocument.ParseAsync(fs); 62 | 63 | var names = document.RootElement 64 | .EnumerateObject() 65 | .Select(p => p.Name) 66 | .ToList(); 67 | 68 | return names; 69 | } 70 | 71 | public async Task SaveTranslation(string key, Language language, string value) 72 | { 73 | var filePath = $@"{TranslationsPath}/translations.{language.GetIsoCode()}.json"; 74 | var options = new JsonSerializerOptions { WriteIndented = true }; 75 | if (!File.Exists(filePath)) 76 | { 77 | var translationObject = new JsonObject 78 | { 79 | [key] = value 80 | }; 81 | await File.WriteAllTextAsync(filePath, translationObject.ToJsonString(options)); 82 | } 83 | else 84 | { 85 | var jsonString = await File.ReadAllTextAsync(filePath); 86 | var jsonObject = JsonNode.Parse(jsonString); 87 | jsonObject![key] = value; 88 | await File.WriteAllTextAsync(filePath, jsonObject.ToJsonString(options)); 89 | } 90 | 91 | if (cache is not null) await cache.Remove($"{language}"); 92 | } 93 | 94 | public async Task RemoveTranslation(string key) 95 | { 96 | var configSupportedLanguages = ((ITranslationsStore)this).Config?.SupportedLanguages; 97 | if (configSupportedLanguages == null) return; 98 | foreach (var lang in configSupportedLanguages) 99 | { 100 | var filePath = $@"{TranslationsPath}/translations.{lang.GetIsoCode()}.json"; 101 | if (!File.Exists(filePath)) continue; 102 | var options = new JsonSerializerOptions { WriteIndented = true }; 103 | var jsonString = await File.ReadAllTextAsync(filePath); 104 | var jsonObject = JsonNode.Parse(jsonString); 105 | if (jsonObject is null) continue; 106 | (jsonObject as JsonObject)?.Remove(key); 107 | await File.WriteAllTextAsync(filePath, jsonObject.ToJsonString(options)); 108 | if (cache is not null) await cache.Remove($"{lang}"); 109 | } 110 | } 111 | 112 | public async Task> GetTranslationsCount() 113 | { 114 | var translationsCount = new List<(Language lang, int count)>(); 115 | var configSupportedLanguages = ((ITranslationsStore)this).Config?.SupportedLanguages; 116 | if (configSupportedLanguages == null) return translationsCount; 117 | foreach (var lang in configSupportedLanguages) 118 | { 119 | var filePath = $@"{TranslationsPath}/translations.{lang.GetIsoCode()}.json"; 120 | if (!File.Exists(filePath)) 121 | { 122 | translationsCount.Add((lang, 0)); 123 | continue; 124 | } 125 | 126 | await using var fs = new FileStream(filePath, FileMode.Open); 127 | using var document = await JsonDocument.ParseAsync(fs); 128 | 129 | var count = document.RootElement 130 | .EnumerateObject().Count(); 131 | translationsCount.Add((lang, count)); 132 | } 133 | 134 | return translationsCount; 135 | } 136 | } -------------------------------------------------------------------------------- /translord.Manager/Components/Account/Pages/Manage/ExternalLogins.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/Manage/ExternalLogins" 2 | 3 | @using Microsoft.AspNetCore.Authentication 4 | @using Microsoft.AspNetCore.Identity 5 | @using translord.Manager.Data 6 | 7 | @inject UserManager UserManager 8 | @inject SignInManager SignInManager 9 | @inject IdentityUserAccessor UserAccessor 10 | @inject IUserStore UserStore 11 | @inject IdentityRedirectManager RedirectManager 12 | 13 | Manage your external logins 14 | 15 | 16 | @if (currentLogins?.Count > 0) 17 | { 18 |

Registered Logins

19 | 20 | 21 | @foreach (var login in currentLogins) 22 | { 23 | 24 | 25 | 42 | 43 | } 44 | 45 |
@login.ProviderDisplayName 26 | @if (showRemoveButton) 27 | { 28 |
29 | 30 |
31 | 32 | 33 | 34 |
35 | 36 | } 37 | else 38 | { 39 | @:   40 | } 41 |
46 | } 47 | @if (otherLogins?.Count > 0) 48 | { 49 |

Add another service to log in.

50 |
51 |
52 | 53 |
54 |

55 | @foreach (var provider in otherLogins) 56 | { 57 | 60 | } 61 |

62 |
63 | 64 | } 65 | 66 | @code { 67 | public const string LinkLoginCallbackAction = "LinkLoginCallback"; 68 | 69 | private ApplicationUser user = default!; 70 | private IList? currentLogins; 71 | private IList? otherLogins; 72 | private bool showRemoveButton; 73 | 74 | [CascadingParameter] private HttpContext HttpContext { get; set; } = default!; 75 | 76 | [SupplyParameterFromForm] private string? LoginProvider { get; set; } 77 | 78 | [SupplyParameterFromForm] private string? ProviderKey { get; set; } 79 | 80 | [SupplyParameterFromQuery] private string? Action { get; set; } 81 | 82 | protected override async Task OnInitializedAsync() 83 | { 84 | user = await UserAccessor.GetRequiredUserAsync(HttpContext); 85 | currentLogins = await UserManager.GetLoginsAsync(user); 86 | otherLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync()) 87 | .Where(auth => currentLogins.All(ul => auth.Name != ul.LoginProvider)) 88 | .ToList(); 89 | 90 | string? passwordHash = null; 91 | if (UserStore is IUserPasswordStore userPasswordStore) 92 | { 93 | passwordHash = await userPasswordStore.GetPasswordHashAsync(user, HttpContext.RequestAborted); 94 | } 95 | 96 | showRemoveButton = passwordHash is not null || currentLogins.Count > 1; 97 | 98 | if (HttpMethods.IsGet(HttpContext.Request.Method) && Action == LinkLoginCallbackAction) 99 | { 100 | await OnGetLinkLoginCallbackAsync(); 101 | } 102 | } 103 | 104 | private async Task OnSubmitAsync() 105 | { 106 | var result = await UserManager.RemoveLoginAsync(user, LoginProvider!, ProviderKey!); 107 | if (!result.Succeeded) 108 | { 109 | RedirectManager.RedirectToCurrentPageWithStatus("Error: The external login was not removed.", HttpContext); 110 | } 111 | 112 | await SignInManager.RefreshSignInAsync(user); 113 | RedirectManager.RedirectToCurrentPageWithStatus("The external login was removed.", HttpContext); 114 | } 115 | 116 | private async Task OnGetLinkLoginCallbackAsync() 117 | { 118 | var userId = await UserManager.GetUserIdAsync(user); 119 | var info = await SignInManager.GetExternalLoginInfoAsync(userId); 120 | if (info is null) 121 | { 122 | RedirectManager.RedirectToCurrentPageWithStatus("Error: Could not load external login info.", HttpContext); 123 | } 124 | 125 | var result = await UserManager.AddLoginAsync(user, info); 126 | if (!result.Succeeded) 127 | { 128 | RedirectManager.RedirectToCurrentPageWithStatus("Error: The external login was not added. External logins can only be associated with one account.", HttpContext); 129 | } 130 | 131 | // Clear the existing external cookie to ensure a clean login process 132 | await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); 133 | 134 | RedirectManager.RedirectToCurrentPageWithStatus("The external login was added.", HttpContext); 135 | } 136 | 137 | } -------------------------------------------------------------------------------- /translord/Enums/Language.cs: -------------------------------------------------------------------------------- 1 | namespace translord.Enums; 2 | 3 | public enum Language 4 | { 5 | EnglishBritish = 1, 6 | Polish = 2, 7 | German = 3, 8 | French = 4, 9 | Spanish = 5, 10 | Italian = 6, 11 | Ukrainian = 7, 12 | Czech = 8, 13 | Japanese = 9, 14 | Bulgarian = 10, 15 | Danish = 11, 16 | Greek = 12, 17 | EnglishAmerican = 13, 18 | Estonian = 14, 19 | Finnish = 15, 20 | Hungarian = 16, 21 | Indonesian = 17, 22 | Korean = 18, 23 | Lithuanian = 19, 24 | Latvian = 20, 25 | Norwegian = 21, 26 | Dutch = 22, 27 | PortugueseBrazilian = 23, 28 | PortugueseEuropean = 24, 29 | Romanian = 25, 30 | Russian = 26, 31 | Slovak = 27, 32 | Slovenian = 28, 33 | Swedish = 29, 34 | Turkish = 30, 35 | ChineseSimplified = 31, 36 | } 37 | 38 | public static class LanguageExtensions 39 | { 40 | public static string GetIsoCode(this Language language) 41 | { 42 | return language switch 43 | { 44 | Language.EnglishBritish => "en-gb", 45 | Language.Polish => "pl", 46 | Language.German => "de", 47 | Language.French => "fr", 48 | Language.Spanish => "es", 49 | Language.Italian => "it", 50 | Language.Ukrainian => "uk", 51 | Language.Czech => "cs", 52 | Language.Japanese => "ja", 53 | Language.Bulgarian => "bg", 54 | Language.Danish => "da", 55 | Language.Greek => "el", 56 | Language.EnglishAmerican => "en-us", 57 | Language.Estonian => "et", 58 | Language.Finnish => "fi", 59 | Language.Hungarian => "hu", 60 | Language.Indonesian => "id", 61 | Language.Korean => "ko", 62 | Language.Lithuanian => "lt", 63 | Language.Latvian => "lv", 64 | Language.Norwegian => "nb", 65 | Language.Dutch => "nl", 66 | Language.PortugueseBrazilian => "pt-br", 67 | Language.PortugueseEuropean => "pt-pt", 68 | Language.Romanian => "ro", 69 | Language.Russian => "ru", 70 | Language.Slovak => "sk", 71 | Language.Slovenian => "sl", 72 | Language.Swedish => "sv", 73 | Language.Turkish => "tr", 74 | Language.ChineseSimplified => "zh", 75 | _ => throw new NotImplementedException("Language is not implemented yet.") 76 | }; 77 | } 78 | 79 | 80 | public static Language FromIsoCode(this string languageIsoCode) 81 | { 82 | return languageIsoCode switch 83 | { 84 | "en-gb" => Language.EnglishBritish, 85 | "pl" => Language.Polish, 86 | "de" => Language.German, 87 | "fr" => Language.French, 88 | "es" => Language.Spanish, 89 | "it" => Language.Italian, 90 | "uk" => Language.Ukrainian, 91 | "cs" => Language.Czech, 92 | "ja" => Language.Japanese, 93 | "bg" => Language.Bulgarian, 94 | "da" => Language.Danish, 95 | "el" => Language.Greek, 96 | "en-us" => Language.EnglishAmerican, 97 | "et" => Language.Estonian, 98 | "fi" => Language.Finnish, 99 | "hu" => Language.Hungarian, 100 | "id" => Language.Indonesian, 101 | "ko" => Language.Korean, 102 | "lt" => Language.Lithuanian, 103 | "lv" => Language.Latvian, 104 | "nb" => Language.Norwegian, 105 | "nl" => Language.Dutch, 106 | "pt-br" => Language.PortugueseBrazilian, 107 | "pt-pt" => Language.PortugueseEuropean, 108 | "ro" => Language.Romanian, 109 | "ru" => Language.Russian, 110 | "sk" => Language.Slovak, 111 | "sl" => Language.Slovenian, 112 | "sv" => Language.Swedish, 113 | "tr" => Language.Turkish, 114 | "zh" => Language.ChineseSimplified, 115 | _ => throw new NotImplementedException("Language is not implemented yet.") 116 | }; 117 | } 118 | 119 | public static string GetSourceIsoCode(this Language language) 120 | { 121 | return language switch 122 | { 123 | Language.EnglishBritish or Language.EnglishAmerican => "en", 124 | Language.PortugueseBrazilian or Language.PortugueseEuropean => "pt", 125 | _ => GetIsoCode(language) 126 | }; 127 | } 128 | 129 | public static string GetName(this Language language) 130 | { 131 | return language switch 132 | { 133 | Language.EnglishBritish => "English (British)", 134 | Language.Polish => "Polish", 135 | Language.German => "German", 136 | Language.French => "French", 137 | Language.Spanish => "Spanish", 138 | Language.Italian => "Italian", 139 | Language.Ukrainian => "Ukrainian", 140 | Language.Czech => "Czech", 141 | Language.Japanese => "Japanese", 142 | Language.Bulgarian => "Bulgarian", 143 | Language.Danish => "Danish", 144 | Language.Greek => "Greek", 145 | Language.EnglishAmerican => "English (American)", 146 | Language.Estonian => "Estonian", 147 | Language.Finnish => "Finnish", 148 | Language.Hungarian => "Hungarian", 149 | Language.Indonesian => "Indonesian", 150 | Language.Korean => "Korean", 151 | Language.Lithuanian => "Lithuanian", 152 | Language.Latvian => "Latvian", 153 | Language.Norwegian => "Norwegian", 154 | Language.Dutch => "Dutch", 155 | Language.PortugueseBrazilian => "Portuguese (Brazilian)", 156 | Language.PortugueseEuropean => "Portuguese (European)", 157 | Language.Romanian => "Romanian", 158 | Language.Russian => "Russian", 159 | Language.Slovak => "Slovak", 160 | Language.Slovenian => "Slovenian", 161 | Language.Swedish => "Swedish", 162 | Language.Turkish => "Turkish", 163 | Language.ChineseSimplified => "Chinese (Simplified)", 164 | _ => throw new NotImplementedException("Language is not implemented yet.") 165 | }; 166 | } 167 | } --------------------------------------------------------------------------------