Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add globalization sample #4414

Merged
merged 16 commits into from
Feb 18, 2023
1 change: 1 addition & 0 deletions samples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ The following samples and guidance demonstrate how to use .NET and Docker for de
* [Build a .NET Docker image](dotnetapp/README.md)
* [Build an ASP.NET Core Docker image](aspnetapp/README.md)
* [Build and test a multi-project solution](complexapp/README.md)
* [Building a globalization aware (or unaware) image](globalapp/README.md)

## Development guidance

Expand Down
30 changes: 30 additions & 0 deletions samples/globalapp/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
ARG TAG=7.0
# ARG TAG=7.0-jammy
# ARG TAG=7.0-alpine
FROM mcr.microsoft.com/dotnet/sdk:$TAG AS build
WORKDIR /source

# copy csproj and restore as distinct layers
COPY *.csproj .
RUN dotnet restore --use-current-runtime

# copy and publish app and libraries
COPY . .
RUN dotnet publish -c Release -o /app --use-current-runtime --self-contained false --no-restore

# final stage/image
FROM mcr.microsoft.com/dotnet/runtime:$TAG
# For Ubuntu
# RUN apt update && DEBIAN_FRONTEND=noninteractive && apt install -y tzdata
# For Alpine
# ENV \
# DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false \
# LC_ALL=en_US.UTF-8 \
# LANG=en_US.UTF-8
# RUN apk add --no-cache \
# icu-data-full \
# icu-libs \
# tzdata
WORKDIR /app
COPY --from=build /app .
ENTRYPOINT ["./globalapp"]
105 changes: 105 additions & 0 deletions samples/globalapp/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
using System.Globalization;
using static System.Console;

// Hello world, indeed
WriteLine("Hello, World!");

DateTime nowUtc = DateTime.UtcNow;
DateTime now = DateTime.Now;

// Accessing UTC and Local time will always work
PrintHeader("Print baseline timezones");
WriteLine($"{nameof(TimeZoneInfo.Utc)}: {TimeZoneInfo.Utc}; {nowUtc}");
// Local will match UTC unless `$TZ` or `/etc/timezone` is set to another timezone
// This is demonstrated in the Dockerfile
WriteLine($"{nameof(TimeZoneInfo.Local)}: {TimeZoneInfo.Local}; {now}");

// Code after this point:
// Requires ICU and tzdata be installed
// Will not work with Globalization invariant mode
// https://aka.ms/GlobalizationInvariantMode

PrintHeader("Print specific timezone");
string home = "America/Los_Angeles";
var tz = TimeZoneInfo.FindSystemTimeZoneById(home);
DateTime nowAtHome = TimeZoneInfo.ConvertTimeFromUtc(nowUtc, tz);
WriteLine($"Home timezone: {home}");
WriteLine($"DateTime at home: {nowAtHome}");

CultureInfo[] cultures =
{
new CultureInfo("en-US"),
new CultureInfo("en-CA"),
new CultureInfo("fr-CA"),
new CultureInfo("hr-HR"),
new CultureInfo("jp-JP"),
new CultureInfo("ko-KR"),
new CultureInfo("pt-BR"),
new CultureInfo("zh-CN")
};

// Print date in correct formats
PrintHeader("Culture-specific dates");
WriteLine($"Current: {nowUtc.ToString("d")}");
foreach (var culture in cultures)
{
PrintDateWithCulture(nowUtc, culture);
}

void PrintDateWithCulture(DateTime dt, CultureInfo culture)
{
WriteLine($"{culture.DisplayName} -- {culture}:");
WriteLine(dt.ToString(culture));
WriteLine(dt.ToString("d", culture));
WriteLine(dt.ToString("t", culture));
}

PrintHeader("Culture-specific currency:");
var currencyValue = 1337;
WriteLine($"Current: {currencyValue.ToString("c")}");
foreach (var culture in cultures)
{
PrintValueWithCurrency(currencyValue, culture);
}

void PrintValueWithCurrency(int value, CultureInfo culture)
{
WriteLine($"{culture}: {value.ToString("c", culture)}");
}

void PrintHeader(string title)
{
WriteLine();
WriteLine($"****{title}**");
}

// https://devblogs.microsoft.com/dotnet/handling-a-new-era-in-the-japanese-calendar-in-net/
// Test Japanese calendar
PrintHeader("Japanese calendar");
var cal = new JapaneseCalendar();
var jaJP = new CultureInfo("ja-JP");
jaJP.DateTimeFormat.Calendar = cal;

var date = cal.ToDateTime(31, 8, 18, 0, 0, 0, 0, 4);
WriteLine($"{date:d}");
WriteLine($"{date.ToString("d", jaJP)}");

var date89 = new DateTime(1989, 8, 18);
jaJP.DateTimeFormat.Calendar = cal;
CultureInfo.CurrentCulture = jaJP;

WriteLine($"{date89:ggy年M月d日}");
WriteLine($"{date89:ggy'年'M'月'd'日'}");

PrintHeader("String comparison");
WriteLine("Comparison results: `0` mean equal, `-1` is less than and `1` is greater");
CompareInfo turkishCompare = new CultureInfo("tr-TR").CompareInfo;
CompareInfo englishCompare = new CultureInfo("en-US").CompareInfo;

WriteLine("Test: compare i to (Turkish) \u0130; first test should be equal and second not");
WriteLine(turkishCompare.Compare("i", "\u0130", CompareOptions.IgnoreCase)); // print 0 as "i" is equal to Turkish I "\u0130".
WriteLine(englishCompare.Compare("i", "\u0130", CompareOptions.IgnoreCase)); // print -1 as "i" is not equal to Turkish I "\u0130" in non-Turkish cultures.

// Å LATIN CAPITAL LETTER A WITH RING ABOVE is equal to 'A' + '\u030A'
WriteLine("Test: compare \u00C5 A\u030A; should be equal");
WriteLine(englishCompare.Compare("\u00C5", "A\u030A"));
102 changes: 102 additions & 0 deletions samples/globalapp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Globalization Test App

This [app](Program.cs) is intended for testing .NET APIs that use globalization APIs and concepts. The behavior of these APIs varies depending on how a container (or other environment) is configured. The [Dockerfile](Dockerfile) is intended to aid testing and isn't itself an example of a production Dockerfile.

The behavior of these APIs is affected by:

- Presence of `icu` and `tzdata` packages.
- The value of [globalization invariant mode](https://aka.ms/GlobalizationInvariantMode).
- The value (or absence) of the `TZ` environment variable.
- The value (or absence) of `/etc/timezone`
richlander marked this conversation as resolved.
Show resolved Hide resolved

The recommended way to configure tzdata and timezones is to set the container timezone by using the `TZ` environment variables, as is demonstrated below.

```bash
$ docker run --rm -it -e TZ="Etc/UTC" app
$ docker run --rm -it -e TZ=$(cat /etc/timezone) app
```

The first approach passes the `Etc/UTC` timezone directly, while the second approach passes the host timezone to the container.

A machine configured to UTC will produce the following:

```bash
# cat /etc/timezone
Etc/UTC
```

The app produces the following output, for "America/Los_Angeles" timezone:
richlander marked this conversation as resolved.
Show resolved Hide resolved

```bash
$ docker build --pull -t app .
$ docker run --rm -it -e TZ="America/Los_Angeles" app
Hello, World!

****Print baseline timezones**
Utc: (UTC) Coordinated Universal Time; 2/14/2023 12:09:26AM
Local: (UTC-08:00) Pacific Time (Los Angeles); 2/13/2023 4:09:26PM

****Print specific timezone**
Home timezone: America/Los_Angeles
DateTime at home: 2/13/2023 4:09:26PM

****Culture-specific dates**
Current: 2/14/2023
English (United States) -- en-US:
2/14/2023 12:09:26AM
2/14/2023
12:09AM
English (Canada) -- en-CA:
2/14/2023 12:09:26a.m.
2/14/2023
12:09a.m.
French (Canada) -- fr-CA:
2023-02-14 00 h 09 min 26 s
2023-02-14
00 h 09
Croatian (Croatia) -- hr-HR:
14. 02. 2023. 00:09:26
14. 02. 2023.
00:09
jp (Japan) -- jp-JP:
2/14/2023 00:09:26
2/14/2023
00:09
Korean (South Korea) -- ko-KR:
2023. 2. 14. 오전 12:09:26
2023. 2. 14.
오전 12:09
Portuguese (Brazil) -- pt-BR:
14/02/2023 00:09:26
14/02/2023
00:09
Chinese (China) -- zh-CN:
2023/2/14 00:09:26
2023/2/14
00:09

****Culture-specific currency:**
Current: $1,337.00
en-US: $1,337.00
en-CA: $1,337.00
fr-CA: 1 337,00 $
hr-HR: 1.337,00 €
jp-JP: ¥1,337
ko-KR: ₩1,337
pt-BR: R$ 1.337,00
zh-CN: ¥1,337.00

****Japanese calendar**
8/18/2019
01/08/18
平成元年8月18日
平成元年8月18日

****String comparison**
Comparison results: `0` mean equal, `-1` is less than and `1` is greater
Test: compare i to (Turkish) İ; first test should be equal and second not
0
-1
Test: compare Å Å; should be equal
0
```
10 changes: 10 additions & 0 deletions samples/globalapp/globalapp.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

</Project>