Add translation framework and example data
This commit is contained in:
parent
238752a5b3
commit
81b065c358
@ -134,6 +134,24 @@
|
|||||||
<Content Update="wwwroot\data\datatable\wordlist.bin">
|
<Content Update="wwwroot\data\datatable\wordlist.bin">
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
</Content>
|
</Content>
|
||||||
|
<Content Update="wwwroot\data\datatable-bak\don_cos_reward.bin">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
<Content Update="wwwroot\data\datatable-bak\musicinfo.bin">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
<Content Update="wwwroot\data\datatable-bak\music_order.bin">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
<Content Update="wwwroot\data\datatable-bak\neiro.bin">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
<Content Update="wwwroot\data\datatable-bak\shougou.bin">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
<Content Update="wwwroot\data\datatable-bak\wordlist.bin">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
62
TaikoWebUI/Localization/LocalizationResource.Designer.cs
generated
Normal file
62
TaikoWebUI/Localization/LocalizationResource.Designer.cs
generated
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// <auto-generated>
|
||||||
|
// This code was generated by a tool.
|
||||||
|
//
|
||||||
|
// Changes to this file may cause incorrect behavior and will be lost if
|
||||||
|
// the code is regenerated.
|
||||||
|
// </auto-generated>
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
namespace TaikoWebUI.Localization {
|
||||||
|
using System;
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A strongly-typed resource class, for looking up localized strings, etc.
|
||||||
|
/// </summary>
|
||||||
|
// This class was auto-generated by the StronglyTypedResourceBuilder
|
||||||
|
// class via a tool like ResGen or Visual Studio.
|
||||||
|
// To add or remove a member, edit your .ResX file then rerun ResGen
|
||||||
|
// with the /str option, or rebuild your VS project.
|
||||||
|
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
|
||||||
|
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||||
|
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||||
|
internal class LocalizationResource {
|
||||||
|
|
||||||
|
private static global::System.Resources.ResourceManager resourceMan;
|
||||||
|
|
||||||
|
private static global::System.Globalization.CultureInfo resourceCulture;
|
||||||
|
|
||||||
|
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
|
||||||
|
internal LocalizationResource() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the cached ResourceManager instance used by this class.
|
||||||
|
/// </summary>
|
||||||
|
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||||
|
internal static global::System.Resources.ResourceManager ResourceManager {
|
||||||
|
get {
|
||||||
|
if (object.ReferenceEquals(resourceMan, null)) {
|
||||||
|
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("TaikoWebUI.Localization.LocalizationResource", typeof(LocalizationResource).Assembly);
|
||||||
|
resourceMan = temp;
|
||||||
|
}
|
||||||
|
return resourceMan;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Overrides the current thread's CurrentUICulture property for all
|
||||||
|
/// resource lookups using this strongly typed resource class.
|
||||||
|
/// </summary>
|
||||||
|
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||||
|
internal static global::System.Globalization.CultureInfo Culture {
|
||||||
|
get {
|
||||||
|
return resourceCulture;
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
resourceCulture = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
23
TaikoWebUI/Localization/LocalizationResource.en-US.resx
Normal file
23
TaikoWebUI/Localization/LocalizationResource.en-US.resx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<root>
|
||||||
|
<resheader name="resmimetype">
|
||||||
|
<value>text/microsoft-resx</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="version">
|
||||||
|
<value>1.3</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="reader">
|
||||||
|
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="writer">
|
||||||
|
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<data name="dashboard" xml:space="preserve">
|
||||||
|
<value>Dashboard</value>
|
||||||
|
</data>
|
||||||
|
<data name="users" xml:space="preserve">
|
||||||
|
<value>Users</value>
|
||||||
|
</data>
|
||||||
|
<data name="edit profile" xml:space="preserve">
|
||||||
|
<value>Edit Profile</value>
|
||||||
|
</data>
|
||||||
|
</root>
|
21
TaikoWebUI/Localization/LocalizationResource.resx
Normal file
21
TaikoWebUI/Localization/LocalizationResource.resx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<root>
|
||||||
|
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||||
|
<xsd:element name="root" msdata:IsDataSet="true">
|
||||||
|
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:schema>
|
||||||
|
<resheader name="resmimetype">
|
||||||
|
<value>text/microsoft-resx</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="version">
|
||||||
|
<value>1.3</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="reader">
|
||||||
|
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="writer">
|
||||||
|
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
</root>
|
23
TaikoWebUI/Localization/LocalizationResource.zh-Hans.resx
Normal file
23
TaikoWebUI/Localization/LocalizationResource.zh-Hans.resx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<root>
|
||||||
|
<resheader name="resmimetype">
|
||||||
|
<value>text/microsoft-resx</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="version">
|
||||||
|
<value>1.3</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="reader">
|
||||||
|
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="writer">
|
||||||
|
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<data name="dashboard" xml:space="preserve">
|
||||||
|
<value>主页</value>
|
||||||
|
</data>
|
||||||
|
<data name="users" xml:space="preserve">
|
||||||
|
<value>用户管理</value>
|
||||||
|
</data>
|
||||||
|
<data name="edit profile" xml:space="preserve">
|
||||||
|
<value>编辑档案</value>
|
||||||
|
</data>
|
||||||
|
</root>
|
@ -1,6 +1,6 @@
|
|||||||
@page "/"
|
@page "/"
|
||||||
|
|
||||||
<h1>Dashboard</h1>
|
<h1>@Localizer["dashboard"]</h1>
|
||||||
|
|
||||||
<MudText Class="mt-8">
|
<MudText Class="mt-8">
|
||||||
Welcome to TaikoWebUI!
|
Welcome to TaikoWebUI!
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
@page "/Users"
|
@page "/Users"
|
||||||
|
|
||||||
<h1>Users</h1>
|
<h1>@Localizer["users"]</h1>
|
||||||
<MudGrid Class="my-8">
|
<MudGrid Class="my-8">
|
||||||
@if (response is null)
|
@if (response is null)
|
||||||
{
|
{
|
||||||
@ -94,7 +94,7 @@
|
|||||||
<MudButton Href="@($"Users/{user.Baid}/Profile")"
|
<MudButton Href="@($"Users/{user.Baid}/Profile")"
|
||||||
Size="Size.Small" Variant="Variant.Text" StartIcon="@Icons.Material.Filled.Edit"
|
Size="Size.Small" Variant="Variant.Text" StartIcon="@Icons.Material.Filled.Edit"
|
||||||
Color="Color.Primary">
|
Color="Color.Primary">
|
||||||
Edit Profile
|
@Localizer["edit profile"]
|
||||||
</MudButton>
|
</MudButton>
|
||||||
<MudMenu Size="Size.Small"
|
<MudMenu Size="Size.Small"
|
||||||
Dense="true"
|
Dense="true"
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
|
using System.Globalization;
|
||||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||||
|
using Microsoft.JSInterop;
|
||||||
using MudBlazor.Services;
|
using MudBlazor.Services;
|
||||||
using TaikoWebUI.Settings;
|
using TaikoWebUI.Settings;
|
||||||
|
|
||||||
@ -16,10 +18,30 @@ builder.Services.AddSingleton<IGameDataService, GameDataService>();
|
|||||||
builder.Services.Configure<WebUiSettings>(builder.Configuration.GetSection(nameof(WebUiSettings)));
|
builder.Services.Configure<WebUiSettings>(builder.Configuration.GetSection(nameof(WebUiSettings)));
|
||||||
|
|
||||||
builder.Services.AddScoped<LoginService>();
|
builder.Services.AddScoped<LoginService>();
|
||||||
|
builder.Services.AddLocalization();
|
||||||
|
builder.Services.AddSingleton<MudLocalizer, ResXMudLocalizer>();
|
||||||
|
|
||||||
var host = builder.Build();
|
var host = builder.Build();
|
||||||
|
|
||||||
var gameDataService = host.Services.GetRequiredService<IGameDataService>();
|
var gameDataService = host.Services.GetRequiredService<IGameDataService>();
|
||||||
await gameDataService.InitializeAsync(builder.HostEnvironment.BaseAddress);
|
await gameDataService.InitializeAsync(builder.HostEnvironment.BaseAddress);
|
||||||
|
|
||||||
|
CultureInfo culture;
|
||||||
|
var js = host.Services.GetRequiredService<IJSRuntime>();
|
||||||
|
var result = await js.InvokeAsync<string?>("blazorCulture.get");
|
||||||
|
|
||||||
|
if (result is not null)
|
||||||
|
{
|
||||||
|
culture = new CultureInfo(result);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
culture = new CultureInfo("en-US");
|
||||||
|
await js.InvokeVoidAsync("blazorCulture.set", "en-US");
|
||||||
|
}
|
||||||
|
|
||||||
|
CultureInfo.DefaultThreadCurrentCulture = culture;
|
||||||
|
CultureInfo.DefaultThreadCurrentUICulture = culture;
|
||||||
|
|
||||||
|
|
||||||
await host.RunAsync();
|
await host.RunAsync();
|
17
TaikoWebUI/ResXMudLocalizer.cs
Normal file
17
TaikoWebUI/ResXMudLocalizer.cs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
using TaikoWebUI.Localization;
|
||||||
|
using Microsoft.Extensions.Localization;
|
||||||
|
using MudBlazor;
|
||||||
|
namespace TaikoWebUI;
|
||||||
|
|
||||||
|
|
||||||
|
internal class ResXMudLocalizer : MudLocalizer
|
||||||
|
{
|
||||||
|
private IStringLocalizer localization;
|
||||||
|
|
||||||
|
public ResXMudLocalizer(IStringLocalizer<LocalizationResource> localizer)
|
||||||
|
{
|
||||||
|
localization = localizer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override LocalizedString this[string key] => localization[key];
|
||||||
|
}
|
35
TaikoWebUI/Shared/ChooseLanguage.razor
Normal file
35
TaikoWebUI/Shared/ChooseLanguage.razor
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
@using System.Globalization;
|
||||||
|
@inject NavigationManager NavigationManager
|
||||||
|
@inject IJSRuntime JSRuntime
|
||||||
|
|
||||||
|
<MudMenu Icon="@Icons.Material.Filled.Language" Color="Color.Inherit">
|
||||||
|
@foreach (var culture in SupportedCultures)
|
||||||
|
{
|
||||||
|
<MudMenuItem OnClick="() => RequestCultureChange(culture.Key)">@culture.Value</MudMenuItem>
|
||||||
|
}
|
||||||
|
</MudMenu>
|
||||||
|
|
||||||
|
|
||||||
|
@code{
|
||||||
|
public readonly Dictionary<CultureInfo, string> SupportedCultures = new()
|
||||||
|
{
|
||||||
|
{ new CultureInfo("en-US"), "English" },
|
||||||
|
{ new CultureInfo("zh-Hans"), "简体中文" },
|
||||||
|
// TODO: Add JP localization?
|
||||||
|
//{ new CultureInfo("ja"), "日本語" },
|
||||||
|
};
|
||||||
|
|
||||||
|
private void RequestCultureChange(CultureInfo newCulture)
|
||||||
|
{
|
||||||
|
if (Equals(CultureInfo.CurrentCulture, newCulture))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var js = (IJSInProcessRuntime)JSRuntime;
|
||||||
|
js.InvokeVoid("blazorCulture.set", newCulture.Name);
|
||||||
|
NavigationManager.NavigateTo(NavigationManager.Uri, forceLoad: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,8 @@
|
|||||||
<MudLayout>
|
<MudLayout>
|
||||||
<MudAppBar Elevation="0">
|
<MudAppBar Elevation="0">
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.Menu" Color="Color.Inherit" Edge="Edge.Start" OnClick="@((e) => DrawerToggle())"/>
|
<MudIconButton Icon="@Icons.Material.Filled.Menu" Color="Color.Inherit" Edge="Edge.Start" OnClick="@((e) => DrawerToggle())"/>
|
||||||
|
<MudSpacer/>
|
||||||
|
<ChooseLanguage/>
|
||||||
</MudAppBar>
|
</MudAppBar>
|
||||||
<MudDrawer @bind-Open="_drawerOpen" Elevation="1">
|
<MudDrawer @bind-Open="_drawerOpen" Elevation="1">
|
||||||
<MudDrawerHeader>
|
<MudDrawerHeader>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<MudNavMenu>
|
<MudNavMenu>
|
||||||
<MudNavLink Href="/" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.Dashboard">Dashboard</MudNavLink>
|
<MudNavLink Href="/" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.Dashboard">@Localizer["dashboard"]</MudNavLink>
|
||||||
<MudNavLink Href="/Users" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.CreditCard">Users</MudNavLink>
|
<MudNavLink Href="/Users" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.CreditCard">@Localizer["users"]</MudNavLink>
|
||||||
</MudNavMenu>
|
</MudNavMenu>
|
@ -7,6 +7,8 @@
|
|||||||
<LangVersion>11</LangVersion>
|
<LangVersion>11</LangVersion>
|
||||||
<WasmEnableSIMD>false</WasmEnableSIMD>
|
<WasmEnableSIMD>false</WasmEnableSIMD>
|
||||||
<WasmEnableExceptionHandling>false</WasmEnableExceptionHandling>
|
<WasmEnableExceptionHandling>false</WasmEnableExceptionHandling>
|
||||||
|
<BlazorWebAssemblyLoadAllGlobalizationData>true</BlazorWebAssemblyLoadAllGlobalizationData>
|
||||||
|
<InvariantGlobalization>false</InvariantGlobalization>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@ -14,6 +16,8 @@
|
|||||||
<PackageReference Include="CodeBeam.MudBlazor.Extensions" Version="6.5.10" />
|
<PackageReference Include="CodeBeam.MudBlazor.Extensions" Version="6.5.10" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.0-rc.1.23421.29" />
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.0-rc.1.23421.29" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.0-rc.1.23421.29" PrivateAssets="all" />
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.0-rc.1.23421.29" PrivateAssets="all" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Localization" Version="8.0.1" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Localization.Abstractions" Version="8.0.1" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0-rc.1.23419.4" />
|
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0-rc.1.23419.4" />
|
||||||
<PackageReference Include="MudBlazor" Version="6.10.0" />
|
<PackageReference Include="MudBlazor" Version="6.10.0" />
|
||||||
<PackageReference Include="SharpZipLib" Version="1.4.2" />
|
<PackageReference Include="SharpZipLib" Version="1.4.2" />
|
||||||
@ -63,5 +67,31 @@
|
|||||||
<_ContentIncludedByDefault Remove="Pages\Pages\Users.razor" />
|
<_ContentIncludedByDefault Remove="Pages\Pages\Users.razor" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<EmbeddedResource Update="Localization\LocalizationResource.en-US.resx">
|
||||||
|
<SubType>Designer</SubType>
|
||||||
|
<Generator>ResXFileCodeGenerator</Generator>
|
||||||
|
<CustomToolNamespace>LocalizationResource</CustomToolNamespace>
|
||||||
|
</EmbeddedResource>
|
||||||
|
<EmbeddedResource Update="Localization\LocalizationResource.zh-Hans.resx">
|
||||||
|
<SubType>Designer</SubType>
|
||||||
|
<Generator>ResXFileCodeGenerator</Generator>
|
||||||
|
<CustomToolNamespace>LocalizationResource</CustomToolNamespace>
|
||||||
|
</EmbeddedResource>
|
||||||
|
<EmbeddedResource Update="Localization\LocalizationResource.resx">
|
||||||
|
<Generator>ResXFileCodeGenerator</Generator>
|
||||||
|
<LastGenOutput>LocalizationResource.Designer.cs</LastGenOutput>
|
||||||
|
</EmbeddedResource>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Update="Localization\LocalizationResource.Designer.cs">
|
||||||
|
<DesignTime>True</DesignTime>
|
||||||
|
<AutoGen>True</AutoGen>
|
||||||
|
<DependentUpon>LocalizationResource.resx</DependentUpon>
|
||||||
|
</Compile>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</Project>
|
</Project>
|
@ -14,4 +14,8 @@
|
|||||||
@using SharedProject.Models
|
@using SharedProject.Models
|
||||||
@using SharedProject.Models.Requests
|
@using SharedProject.Models.Requests
|
||||||
@using SharedProject.Enums
|
@using SharedProject.Enums
|
||||||
@using Throw
|
@using Throw
|
||||||
|
@using Localization
|
||||||
|
@using System.Globalization
|
||||||
|
@using Microsoft.Extensions.Localization
|
||||||
|
@inject IStringLocalizer<LocalizationResource> Localizer
|
@ -31,6 +31,12 @@
|
|||||||
<script src="_content/MudBlazor/MudBlazor.min.js"></script>
|
<script src="_content/MudBlazor/MudBlazor.min.js"></script>
|
||||||
<script src="js/textFit.min.js"></script>
|
<script src="js/textFit.min.js"></script>
|
||||||
<script src="js/updateTextFit.js"></script>
|
<script src="js/updateTextFit.js"></script>
|
||||||
|
<script>
|
||||||
|
window.blazorCulture = {
|
||||||
|
get: () => localStorage['BlazorCulture'],
|
||||||
|
set: (value) => localStorage['BlazorCulture'] = value
|
||||||
|
};
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
Loading…
x
Reference in New Issue
Block a user