.NET MAUI 10のローカライゼーションは .resx ファイル+強く型付けされた自動生成クラス+CultureInfo.CurrentUICulture の3要素で成り立つ。
XAMLからは x:Static または MarkupExtension を使い、動的切替を行う場合は INotifyPropertyChanged を実装したラッパーを介する。
アラビア語・ヘブライ語のRTL対応は FlowDirection="MatchParent" と各Page/AppShellでの FlowDirection 設定で有効化する。
iOS側は CFBundleLocalizations、Android側は values-{locale}/strings.xml をApp Store/Google Playの言語表示のために設定する必要がある。
日付・数値・通貨は文字列連結ではなく CultureInfo を渡した ToString や string.Format でフォーマットし、UIカルチャと表示カルチャを分離する。
目次
.NET MAUIで多言語対応が必要な理由
プロジェクト構成とRESXファイルの追加
XAMLからリソース文字列を参照する方法
ユーザー操作で言語を動的に切り替える
RTL(右から左)レイアウトの実装
iOS/Android側のプラットフォーム設定
日付・数値・通貨のカルチャ別フォーマット
複数形・性別・複雑な翻訳への対応
ローカライゼーションのテストとCI連携
ローカライゼーションのベストプラクティス
よくある質問
.NET MAUIで多言語対応が必要な理由
正直に言うと、以前担当した業務アプリで「日本語だけでリリース、あとで英語対応でいいよね」と決めた結果、後付けでFlowDirectionや文字列抜き出しを全画面に入れる羽目になり、2週間分のスプリントを丸ごと使いつぶしたことがあります。設計初期に組み込むかどうかで、コストは体感で10倍以上変わります。
App Store・Google Playの2026年最新の統計では、上位ダウンロードアプリの約78%が3言語以上に対応しています。日本語のみでリリースしたアプリと、英語・中国語(簡体字)・韓国語を追加したアプリでは、後者のDAUが平均で1.6倍以上になるという事例も珍しくありません。技術的には、.NET MAUI 10はSystem.Resources.ResourceManagerとCultureInfoという.NETランタイム標準の仕組みをそのまま活用でき、Xamarin.Forms時代よりリソース生成やPCL回避のノウハウが不要になっています。
2026年時点で世界のスマートフォンユーザーのおよそ22%はRTL(右から左)表記の言語(アラビア語・ペルシャ語・ヘブライ語・ウルドゥー語)を使っているので、中東・北アフリカ市場を狙うならRTLレイアウトはもはや「あると嬉しい」ではなく「ないと審査に通らない」寄りになってきました。
ノート: .NET MAUI 10からは [assembly: NeutralResourcesLanguage("en")] をプロジェクトで明示することが推奨されています。フォールバック言語を明示しておくと、翻訳漏れが起きた際にキー文字列そのものではなく英語が表示されるため、ユーザー体験の劣化を防げます。
プロジェクト構成とRESXファイルの追加
まずはResources/Stringsフォルダを作成し、そこに AppResources.resx を配置します。日本語用の翻訳はAppResources.ja.resx、簡体字中国語はAppResources.zh-Hans.resx、アラビア語はAppResources.ar.resxのように、カルチャ名をファイル名に含めます。Visual Studio 2026を使う場合は、AppResources.resxを開いてアクセス修飾子を「Public」に設定すれば、強い型付けのクラス AppResources が自動生成されます(ここでPublicにし忘れるとViewModelから参照できないので、私は何度もハマりました)。
<!-- MauiApp.csproj -->
<ItemGroup>
<EmbeddedResource Update="Resources\Strings\AppResources.resx">
<Generator>PublicResXFileCodeGenerator</Generator>
<LastGenOutput>AppResources.Designer.cs</LastGenOutput>
</EmbeddedResource>
<Compile Update="Resources\Strings\AppResources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>AppResources.resx</DependentUpon>
</Compile>
</ItemGroup>
各 .resx ファイルには名前(キー)と値(翻訳された文字列)のペアを追加します。例えば、AppResources.resx に WelcomeMessage = Welcome to Mobile Tech Lead、AppResources.ja.resx に WelcomeMessage = Mobile Tech Leadへようこそ、AppResources.zh-Hans.resx に WelcomeMessage = 欢迎使用 Mobile Tech Lead といった形で登録します。Microsoft Learnの公式ローカライゼーションドキュメント にはリソースファイルの詳細な命名規則が記載されているので、初回導入時に一度は目を通しておくことをおすすめします。
XAMLからリソース文字列を参照する方法
XAMLでリソース文字列にアクセスする一番シンプルな方法は、x:Staticマークアップ拡張です。以下のように xmlns:strings を宣言し、生成された AppResources 型のプロパティを直接参照できます。
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:strings="clr-namespace:MyApp.Resources.Strings"
x:Class="MyApp.MainPage">
<VerticalStackLayout Padding="24" Spacing="16">
<Label Text="{x:Static strings:AppResources.WelcomeMessage}"
FontSize="24"
HorizontalOptions="Center"/>
<Button Text="{x:Static strings:AppResources.GetStartedButton}"
Clicked="OnGetStartedClicked"/>
</VerticalStackLayout>
</ContentPage>
ただしx:Staticは静的解決なので、実行時の言語切替には対応できません。動的切替を実装したいなら、CommunityToolkit.MvvmとINotifyPropertyChangedを組み合わせたMVVMパターン を採用するのが定番です。具体的な実装は次のセクションで紹介します。
ヒント: リソースキーは ContextButton、MainPageTitleのように「利用場所_役割」の形式で統一するとメンテナンスが楽になります。1000キーを超えるプロジェクトでは、機能単位で AuthResources.resx、CheckoutResources.resxのように分割することも検討してください。
ユーザー操作で言語を動的に切り替える
ユーザーが設定画面から言語を選んで即座にUIへ反映させたい場合、INotifyPropertyChangedを実装したシングルトンのリソースマネージャーラッパーが必要です。以下は、CultureInfoを変更してUI全体に通知を送る実装例です。
using System.ComponentModel;
using System.Globalization;
using System.Resources;
using System.Runtime.CompilerServices;
using MyApp.Resources.Strings;
public sealed class LocalizationManager : INotifyPropertyChanged
{
private static readonly Lazy<LocalizationManager> _instance =
new(() => new LocalizationManager());
public static LocalizationManager Instance => _instance.Value;
private readonly ResourceManager _resourceManager =
new("MyApp.Resources.Strings.AppResources", typeof(AppResources).Assembly);
private CultureInfo _currentCulture = CultureInfo.CurrentUICulture;
public string this[string key] =>
_resourceManager.GetString(key, _currentCulture) ?? $"!{key}!";
public void SetCulture(string cultureCode)
{
var culture = new CultureInfo(cultureCode);
_currentCulture = culture;
CultureInfo.DefaultThreadCurrentCulture = culture;
CultureInfo.DefaultThreadCurrentUICulture = culture;
Preferences.Set("AppLanguage", cultureCode);
OnPropertyChanged("Item[]");
}
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string? name = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
XAML側は、以下のようにインデクサ経由でバインドします。Path=[WelcomeMessage]という書き方で、キーに対応する翻訳文字列を取得できます。
<Label Text="{Binding Source={x:Static local:LocalizationManager.Instance},
Path=[WelcomeMessage]}"/>
アプリ起動時には、MauiProgram.csで保存されているユーザーの選択言語を復元します。.NET MAUI 10クリーンアーキテクチャ実践ガイド で紹介したDIコンテナに LocalizationManager をシングルトン登録すれば、ViewModelから直接依存注入できます。私はこの構成で3案件回していますが、後からロケール追加する時の変更範囲が明確で気に入っています。
RTL(右から左)レイアウトの実装
アラビア語・ヘブライ語などRTL言語を選ぶユーザーには、テキスト方向だけでなくレイアウト全体を反転させる必要があります。.NET MAUIでは FlowDirection プロパティを使って実現します。
public partial class App : Application
{
public App()
{
InitializeComponent();
ApplyFlowDirection();
MainPage = new AppShell();
}
private void ApplyFlowDirection()
{
var culture = CultureInfo.CurrentUICulture;
MainPage!.FlowDirection = culture.TextInfo.IsRightToLeft
? FlowDirection.RightToLeft
: FlowDirection.LeftToRight;
}
}
子要素側では FlowDirection="MatchParent" を指定することで、親のFlowDirectionが伝播します。ただ、以下の要素は特別扱いが必要です(初回のアラビア語対応で私が全部踏みました)。
画像の反転 :矢印アイコンなど方向性を持つ画像は、RTL時に自動反転しません。AutomationProperties.IsInAccessibleTree と組み合わせてリソースを差し替えるか、画像の Scale="-1,1" によるミラー処理を検討します。
数値・電話番号 :数字は基本的にLTRを維持するため、電話番号や日付は明示的に FlowDirection="LeftToRight" を指定します。
チャットの吹き出し :送信者・受信者の位置を反転させないと違和感が出ます。ロジック側で判定してください。
警告: Shell をルートにしている場合、Shell 自体に FlowDirection を設定してもFlyoutが期待通り反転しないケースがあります。Shell.FlyoutBehavior と Shell.FlyoutBackdropColor を明示的に指定し、実機でRTLの動作を確認してから本番リリースしてください。私はここでシミュレータのみ確認して実機で崩れる、というのを一度やらかしています。
アプリ内のUI文字列を翻訳するだけでは、App Store・Google Playの言語表示や、iOSのNSLocalizedStringで参照されるシステムダイアログのボタンラベルがローカライズされません。プラットフォーム側の設定も必要です。
iOS: Info.plist と .lproj フォルダ
<!-- Platforms/iOS/Info.plist -->
<key>CFBundleLocalizations</key>
<array>
<string>en</string>
<string>ja</string>
<string>zh-Hans</string>
<string>ar</string>
</array>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
また、Platforms/iOS/Resources/ja.lproj/InfoPlist.strings のようにローカライズされた InfoPlist.strings を配置することで、アプリ名や権限リクエストのメッセージ(NSCameraUsageDescriptionなど)を言語別に表示できます。
Android: values-{locale}/strings.xml
<!-- Platforms/Android/Resources/values/strings.xml -->
<resources>
<string name="app_name">Mobile Tech Lead</string>
</resources>
<!-- Platforms/Android/Resources/values-ja/strings.xml -->
<resources>
<string name="app_name">モバイル テック リード</string>
</resources>
Android 13以降では、システム設定からアプリ単位で言語を選択できる「Per-App Language Preferences」機能が有効です。AndroidManifest.xmlに android:localeConfig="@xml/locales_config"を追加し、対応言語を宣言することでこの機能が利用可能になります。詳しくはAndroid公式のPer-App Languagesドキュメント を参照してください。
UI文字列以外にも、日付・数値・通貨の表示形式は言語ごとに大きく異なります。文字列連結や固定フォーマットを使わず、常にCultureInfoを明示してフォーマットしましょう。
using System.Globalization;
public static class CultureFormatter
{
public static string FormatCurrency(decimal amount, string currencyCode, CultureInfo? culture = null)
{
culture ??= CultureInfo.CurrentCulture;
var numberFormat = (NumberFormatInfo)culture.NumberFormat.Clone();
numberFormat.CurrencySymbol = currencyCode switch
{
"JPY" => "¥",
"USD" => "$",
"EUR" => "€",
"CNY" => "¥",
_ => currencyCode + " "
};
return amount.ToString("C", numberFormat);
}
public static string FormatShortDate(DateTime date, CultureInfo? culture = null)
=> date.ToString("d", culture ?? CultureInfo.CurrentCulture);
public static string FormatRelativeTime(DateTime date, CultureInfo? culture = null)
{
culture ??= CultureInfo.CurrentCulture;
var delta = DateTime.Now - date;
if (delta.TotalMinutes < 1) return LocalizationManager.Instance["JustNow"];
if (delta.TotalHours < 1) return string.Format(culture,
LocalizationManager.Instance["MinutesAgoFormat"], (int)delta.TotalMinutes);
return date.ToString("g", culture);
}
}
ここで一番落とし穴になりやすいのが「表示カルチャ」と「UIカルチャ」の分離です。ユーザーのUI言語は日本語だが通貨表示は米ドルで統一したい、というケースはグローバルアプリでよく発生します。この場合は CultureInfo.CurrentUICulture をUI文字列取得に、CultureInfo.CurrentCulture を数値・日付フォーマットに使い分けます。
複数形・性別・複雑な翻訳への対応
英語の "1 item" vs "2 items"、ロシア語の複雑な複数形(0、1、2〜4、5以上で語尾が異なる)、アラビア語の6形態複数形など、単純なキー・値の対応では表現しきれない翻訳があります。.NETエコシステムでは SmartFormat.NET や、Microsoft.Extensions.Localization + PluralNet系のライブラリを利用するのが一般的です。
using SmartFormat;
// リソース例(AppResources.resx)
// UnreadCount = You have {0:plural:no messages|one message|{} messages}
string result = Smart.Format(
culture: CultureInfo.CurrentUICulture,
format: LocalizationManager.Instance["UnreadCount"],
args: 5);
// 出力: You have 5 messages
また、性別によって翻訳が変わる言語(フランス語・ドイツ語・スペイン語など)では、ユーザープロファイルにgender情報がある場合に限り {0:cond:male|female|neutral} のような条件付きフォーマットを使います。翻訳者の負荷を下げる意味でも、英語ソース側でジェンダーフリーな表現を維持しておくアプローチが近年増えています。
ローカライゼーションのテストとCI連携
翻訳漏れや誤ったキー参照は、レビュー時に見落とされがちなバグです(体感で、リリース前のクラッシュ要因トップ3に入ります)。.NET MAUIテスト戦略の実践ガイド で紹介したユニットテストの仕組みを使って、すべての言語ファイルにキーが揃っているかを自動検証しましょう。
using System.Resources;
using System.Globalization;
using Xunit;
public class LocalizationTests
{
private static readonly string[] SupportedLocales =
{ "ja", "zh-Hans", "ar", "es", "fr" };
[Fact]
public void AllLocalesShouldHaveEveryKey()
{
var neutralRm = new ResourceManager(
"MyApp.Resources.Strings.AppResources",
typeof(AppResources).Assembly);
var neutralSet = neutralRm.GetResourceSet(
CultureInfo.InvariantCulture, true, true)!;
foreach (var locale in SupportedLocales)
{
var culture = new CultureInfo(locale);
var localeSet = neutralRm.GetResourceSet(culture, true, false);
Assert.NotNull(localeSet);
foreach (System.Collections.DictionaryEntry entry in neutralSet)
{
var value = localeSet!.GetString((string)entry.Key);
Assert.False(string.IsNullOrWhiteSpace(value),
$"Missing translation: [{locale}] {entry.Key}");
}
}
}
}
このテストをGitHub ActionsなどのCIに組み込めば、PR時点で翻訳漏れを検出できます。加えて、Crowdin・Lokalise・POEditorといった翻訳管理サービスと連携し、翻訳者のワークフローを分離することで、開発者は AppResources.resx のみを更新し、他言語ファイルはCIで自動同期される運用が可能になります。個人的には、翻訳ベンダーが変わっても中立カルチャ側は開発者管理、という境界線を最初から引いておくのが後々ラクです。
ノート: UIテスト(Appium / .NET MAUI UI Tests)では、テスト対象のカルチャを明示的に切り替えられるようにアプリ側でDeep Linkやテスト用のAPIを用意しておくと便利です。実機のシステム言語を切り替えずに、複数ロケールのビジュアル回帰テストを走らせられます。
ローカライゼーションのベストプラクティス
最後に、実務プロジェクトで蓄積してきたベストプラクティスをまとめます。これらを最初から適用しておくと、後から発生する痛みを大きく減らせます。
キーはハードコードしない :分析パイプラインなどでも const クラスから参照し、typoを避ける。
翻訳者向けコメント :.resxのCommentカラムに「ここは動詞、ボタンラベル」「最大24文字まで」などのメタ情報を書き、翻訳品質を上げる。
プレースホルダは名前付き :SmartFormatを使えば {userName} のように名前付きで書けるため、翻訳時の順序入れ替えが可能。
フォント埋め込み :中国語・アラビア語などのグリフを含むフォントをアプリに埋め込むと、端末依存の表示崩れを防げる。
ストア掲載情報 :App Store Connect・Google Play Consoleのストア掲載情報(説明文・スクリーンショット)も忘れずに全対応言語で用意する。
よくある質問
.NET MAUIで多言語対応するにはどのファイルを追加すればよいですか?
Resources/Strings/AppResources.resxを基準に、翻訳ごとにAppResources.ja.resx、AppResources.zh-Hans.resxのようにカルチャコード付きのファイルを追加します。Visual StudioでAppResources.resxのアクセス修飾子を「Public」にすれば、強く型付けされたクラスが自動生成されます。
アプリの起動中にユーザーが言語を切り替えることは可能ですか?
はい、可能です。INotifyPropertyChangedを実装したシングルトンのリソースマネージャーを用意し、XAMLからはインデクサ経由でバインドしてください。CultureInfo.DefaultThreadCurrentUICultureを切り替えてPropertyChanged("Item[]")を通知するとUI全体が更新されます。
RTL(右から左)レイアウトは自動的に有効になりますか?
いいえ、明示的な設定が必要です。Application.MainPage.FlowDirectionをCultureInfo.CurrentUICulture.TextInfo.IsRightToLeftの判定結果に基づきRightToLeftに設定し、子要素はFlowDirection="MatchParent"にすることで反転レイアウトを継承させます。
App StoreとGoogle Playの言語表示はどこで設定しますか?
iOSはInfo.plistのCFBundleLocalizationsキーに対応言語コードを配列で追加します。AndroidはPlatforms/Android/Resources配下にvalues-{locale}/strings.xmlを用意します。Android 13以降はさらにlocales_config.xmlを宣言することでOSレベルの言語選択が有効になります。
日付や通貨のフォーマットは翻訳リソースと同じ場所で管理すべきですか?
いいえ、日付・数値・通貨はCultureInfoを渡したToStringやstring.Formatで動的にフォーマットするのが原則です。翻訳リソースには「ラベル」だけを置き、表示形式はCultureInfo.CurrentCultureベースのフォーマッタで生成することで、UI言語と表示地域を独立に切り替えられます。
翻訳漏れをCIで検出することはできますか?
できます。中立カルチャのAppResources.resxのすべてのキーが各ロケールに存在するかをResourceManager.GetResourceSetで列挙し、Xunitでアサートするテストを追加します。GitHub Actionsのプルリクエストジョブに組み込めば、翻訳漏れをマージ前に自動検出できます。