IAP 中的货币本地化问题

背景 & 目标

首先需要了解为什么会有这个需求,一般而言,如谷歌和苹果的支付平台,在后台上配置商品价格时,只能选择有限的价格选项,而不是自由指定价格。因此,在面对诸如折扣礼包的需求时,就需要考虑货币本地化的问题了

如:商品原价 100美元,现价 9.9 美元

要注意,这个商品的实际 product id 对应的价格依然是 9.9 美元,而这个 100 美元具体要如何显示,就是本篇文章要介绍的核心问题了

如:在美国会显示为 100$ * 美元汇率(此处为 1),在香港会显示为 HK$100 * 港币汇率 等等

IAP 平台返回的内容

在接入谷歌和苹果的原生 SDK 时,都会提供如下基本信息

  • ISO 4217 货币标准
  • 商品汇率换算后的价格
  • 实际的美元价格

需要注意的是,在 Android 返回中,提供了一个 1,000,000 精度的 getPriceAmountMicros 方法

通过这三个变量信息,我们就获取到了所有关键内容,通过 美元价格 / 汇率换算价格 就可以得到当前货币的实际汇率,而通过 ISO 4217 货币代码,就可以完成货币的本地化显示

CultureInfo

在我们调用 ToString() 方法时,你可能会注意到有如下重载,此处的 IFormatProvider 正是我们接下来需要填入的 CultureInfo

public string ToString(string format, System.IFormatProvider provider);

而微软的 CultureInfo 并不是基于 ISO 4217 标准来建立的,因此我们仍需解决两个标准的映射问题

foreach(CultureInfo culture in CultureInfo.GetCultures(CultureTypes.SpecificCultures))
{
    var region = new RegionInfo(culture.LCID);
    // ISO 4217
    _currency_info.TryAdd(region.ISOCurrencySymbol, new(culture));
}

其他标准

在我们实际使用中,会碰到如下两个标准

第一种一般为一些第三方 SDK 返回 国家/地区信息时用到,而第二种是在调用 Application.systemLanguage 时会用到,因此也会有对应的一些转换需求

// ISO 3166-1 alpha-2
foreach(CultureInfo culture in CultureInfo.GetCultures(CultureTypes.SpecificCultures))
{
    var region = new RegionInfo(culture.LCID);
    Debug.Log(region.TwoLetterISORegionName);
}

但是 SystemLanguage 的转换就比较麻烦了,可以直接拷贝下方代码

private readonly Dictionary<SystemLanguage, CultureInfo> _system_language_info = new()
{
    {SystemLanguage.Afrikaans, new("af-ZA")},
    {SystemLanguage.Arabic, new("ar-SA")},
    {SystemLanguage.Basque, new("eu-ES")},
    {SystemLanguage.Belarusian, new("be-BY")},
    {SystemLanguage.Bulgarian, new("bg-BG")},
    {SystemLanguage.Catalan, new("ca-ES")},
    {SystemLanguage.Chinese, new("zh-CN")},
    {SystemLanguage.ChineseSimplified, new("zh-CN")},
    {SystemLanguage.ChineseTraditional, new("zh-TW")},
    {SystemLanguage.Czech, new("cs-CZ")},
    {SystemLanguage.Danish, new("da-DK")},
    {SystemLanguage.Dutch, new("nl-NL")},
    {SystemLanguage.English, new("en-US")},
    {SystemLanguage.Estonian, new("et-EE")},
    {SystemLanguage.Faroese, new("fo-FO")},
    {SystemLanguage.Finnish, new("fi-FI")},
    {SystemLanguage.French, new("fr-FR")},
    {SystemLanguage.German, new("de-DE")},
    {SystemLanguage.Greek, new("el-GR")},
    {SystemLanguage.Hebrew, new("he-IL")},
    {SystemLanguage.Hungarian, new("hu-HU")},
    {SystemLanguage.Icelandic, new("is-IS")},
    {SystemLanguage.Indonesian, new("id-ID")},
    {SystemLanguage.Italian, new("it-IT")},
    {SystemLanguage.Japanese, new("ja-JP")},
    {SystemLanguage.Korean, new("ko-KR")},
    {SystemLanguage.Latvian, new("lv-LV")},
    {SystemLanguage.Lithuanian, new("lt-LT")},
    {SystemLanguage.Polish, new("pl-PL")},
    {SystemLanguage.Portuguese, new("pt-PT")},
    {SystemLanguage.Romanian, new("ro-RO")},
    {SystemLanguage.Russian, new("ru-RU")},
    {SystemLanguage.Slovak, new("sk-SK")},
    {SystemLanguage.Slovenian, new("sl-SI")},
    {SystemLanguage.Spanish, new("es-ES")},
    {SystemLanguage.Swedish, new("sv-SE")},
    {SystemLanguage.Thai, new("th-TH")},
    {SystemLanguage.Turkish, new("tr-TR")},
    {SystemLanguage.Ukrainian, new("uk-UA")},
    {SystemLanguage.Vietnamese, new("vi-VN")},
    {SystemLanguage.Unknown, new("en-US")}
};

实际使用

public string Localize(double value, int keep_decimal = 2)
{
    // 换算汇率
    value *= _PRICE_RATIO;
    // 最后进行本地化, 此处的 culture_info,需要根据项目实际情况获取
    return value.ToString("C{keep_decimal}", culture_info);
}

参考链接

  1. Android 文档: https://developer.android.com/reference/com/android/billingclient/api/ProductDetails.PricingPhase
  2. iOS 文档: https://developer.apple.com/documentation/storekit/skproduct
  3. CultureInfo 文档: https://learn.microsoft.com/en-us/dotnet/api/system.globalization.cultureinfo?view=net-7.0