Привет! Меня зовут Егор, я работаю в компании DD Planet. В статье я хочу поделиться знаниями о том, как быстро и легко локализовать .NET-приложение с помощью собственного решения для локализации — Slang.Net.

Введение

Когда я только начинал свой путь как .NET-разработчик, локализация приложений казалась вполне понятной задачей. Стандартные механизмы — resx-файлы и связанные с ними сервисы — справлялись со своей работой, и тогда я даже не задумывался о том, что может быть иначе. Однако все изменилось, когда я переключился на разработку Flutter-приложений.

В этом новом для меня мире я открыл slang — инструмент для работы с локализацией, который существенно упростил рутину, связанную с поддержкой нескольких языков. Когда я вернулся к .NET-разработке, мой взгляд на стандартные механизмы локализации изменился. Простота и удобство, которые я нашел в slang, заставили меня осознать, насколько сложными и громоздкими кажутся resx-файлы по сравнению с этим инструментом.

Дальнейшие размышления привели меня к идее создать порт slang для .NET, адаптированный под особенности платформы.

Что такое Slang.Net?

Slang.Net — порт библиотеки slang из сообщества Dart/Flutter для .NET, предоставляющей типобезопасный и удобный способ работы с переводами в приложении. Порт дополнен новыми возможностями, включая форматирование строк, которое отсутствовало в оригинальной версии библиотеки на момент портирования, но было добавлено в одном из последних обновлений.

Основные возможности

  • Типобезопасность: Генерирует строго типизированный код для работы с переводами;

  • Поддержка форматирования: Встроенная поддержка форматирования строк и параметров;

  • Плюрализация: Автоматическая обработка множественных форм слов в зависимости от их количества;

  • Интеграция с GPT: Использование GPT-модели для автоматического перевода строк.

  • Гибкость: Поддержка списков, словарей и вложенных структур переводов;

  • Удобство: Простая интеграция и использование в проекте. Поддержка иерархического представления для строк локалей. Например, можно заложить иерархию по фичам и контроллерам — так, как вам удобнее.

Быстрый старт

Шаг 1: Установка пакета

Установите Slang.Net

dotnet add package Slang.Net

Шаг 2: Добавление JSON-файлов с переводами

Добавьте json файл для локали по умолчанию. Можно положить в любую папку проекта, для примера, положим его в папку i18n:

 • strings_ru.i18n.json

Названия файлов должны иметь следующий вид:
<название>_<культура>.i18n.json

Культура представлена в привычной форме кода языка, с указанием или без указания кода страны (“ru”, “en-US”). Для файла локализации по умолчанию культуру можно не указывать.

Пример содержимого strings_ru.i18n.json:

{
  "greeting": "Привет, {name}!"
}

Шаг 3: Конфигурация Slang.Net

Создайте файл slang.json в корне проекта:

{
  "base_culture": "ru"
}

Шаг 4: Включение файлов в проект

Добавьте следующие строки в ваш .csproj файл:

<ItemGroup>
  <AdditionalFiles Include="i18n\*.i18n.json" />
  <AdditionalFiles Include="slang.json" />
</ItemGroup>

Файлы с типом сборки AdditionalFiles используются анализаторами и генераторами кода и не встраиваются в итоговую сборку проекта.

Шаг 5: Создание partial класса для переводов

Создайте класс с любым названием для доступа к строкам локализации, например, Strings.

В InputFileName указываем префикс из названия json файлов, на основе которых будет генерация для этого класса.

using Slang;

[Translations(InputFileName = "strings")]
public partial class Strings{ }

Можно добавить геттер для быстрого доступа к рутовому узлу словаря локализации

[Translations(InputFileName = "strings")]
public partial class Strings
{
    public static Strings Loc => Instance.Root;
}

Шаг 6: Использование в вашем коде

После генерации кода вы можете использовать переводы следующим образом:

// Установка текущей культуры
Strings.SetCulture(new CultureInfo("ru-RU"));

// Использование перевода
Console.WriteLine(Translations.Instance.Root.Greeting("Иван")); // Выведет: Привет, Иван!

Расширенные возможности

Интерполяция строк

Slang.Net поддерживает интерполяцию и типизацию параметров:

{
  "welcome": "Добро пожаловать, {name:string}!"
}

В коде:

Console.WriteLine(Translations.Instance.Root.Welcome("Мария"));

По умолчанию тип параметра object. Так же есть более сложный синтаксис установки типов параметров:

{
  "greet2": "Hello {name}, you are {age} years old",
  "@greet2": {
    "placeholders": {
      "name": {
        "type": "string"
      },
      "age": {
        "type": "int"
      }
    }
  }
}

Установка отдельного словаря с префиксом @ позволяет использовать еще несколько фич библиотеки, о которых пойдет речь ниже.

Поддержка склонений с числительными (Pluralization)

Обработка множественных форм слов в зависимости от количества:

{
  "apple": {
    "one": "У меня {n} яблоко.",
    "few": "У меня {n} яблока.",
    "many": "У меня {n} яблок.",
    "other": "У меня {n} яблока."
  }
}

В коде:

Console.WriteLine(Translations.Instance.Root.Apple(3)); // Выведет: У меня 3 яблока.

Доступны следующие ключи для работы данной фичи: zero, one, two, few, many, other.

По умолчанию имя параметра n, его можно поменять на свой с помощью param:

{
  "someKey": {
    "apple(param=appleCount)": {
      "one": "I have one apple.",
      "other": "I have multiple apples."
    }
  }
}

Имя параметра по умолчанию можно переопределить, используя параметр PluralParameter в атрибуте TranslationsAttribute:

[Translations(
    InputFileName = "strings",
    PluralParameter = "count")]
internal partial class Strings;

Форматирование строк

Поддержка форматирования дат и чисел. Можно использовать следующие типы с поддержкой форматирования в ToString(string format) - int, long, double, decimal, float, DateTime, DateOnly, TimeOnly, TimeSpan. Для остальных типов используется метод string.Format(format, cultureInfo).

{
  "dateExample": "Date {date}",
  "@dateExample": {
    "placeholders": {
      "date": {
        "type": "DateTime",
        "format": "dd MMMM HH:mm"
      }
    }
  }
}

В коде:

String s = Strings.Instance.Root.DateExample(DateTime.Now); // Date 17 October 22:25

Комментарии в переводах

Вы можете добавлять комментарии в JSON-файлы, которые будут включены в сгенерированный код:

{
  "mainScreen": {
    "button": "Submit",
    // ignored as translation but rendered as a comment
    "@button": "The submit button shown at the bottom",
    // or use 
    "button2": "Submit",
    "@button2": {
      "description": "The submit button shown at the bottom"
    }
  }
}

Тогда сгенерированный код будет включать в себя переопределенный xml комментарий. По умолчанию комментарий включает в себя содержимое строки локализации.

/// The submit button shown at the bottom
///
/// In ru, this message translates to:
/// **"Submit"**
public virtual string Button => "Submit";

Ссылки на другие переводы

Slang.Net поддерживает ссылки на другие строки перевода с помощью @::

{
  "greeting": "Здравствуйте",
  "welcome": "@:greeting, {name}!"
}

В коде:

Console.WriteLine(Translations.Instance.Root.Welcome("Павел")); // Выведет: Здравствуйте, Павел!

Путь на локаль должен быть абсолютным относительно рутового объекта дерева с локалями.

Использование списков и словарей

Slang.Net поддерживает списки и словари:

{
  "menu": {
    "items": [
      "Главная",
      "О нас",
      "Контакты"
    ]
  }
}

В коде:

foreach (var item in Translations.Instance.Root.Menu.Items)
{
    Console.WriteLine(item);
}

Перевод с помощью GPT

Одной из уникальных возможностей Slang.Net является интеграция с моделями GPT для автоматического перевода строк. Эта функция особенно полезна, когда вам нужно быстро локализовать приложение на несколько языков. Для перевода достаточно указать код культуры для перевода, и инструмент сам создаст файл локализации для новой локали или обновит существующий. В настоящее время поддерживается только OpenAI API.

Установка slang-gpt-cli

Для начала установите CLI утилиту с помощью команды:

dotnet tool install -g Slang.CLI

Настройка конфигурации

Создайте или отредактируйте файл slang.json и добавьте следующую конфигурацию:

{
  "base_culture": "ru",
  "gpt": {
    "model": "gpt-4o-mini",
    "description": "Showcase for Slang.Net"
  }
}

Тип модели и описание проекта являются обязательными параметрами для старта перевода.

Запуск перевода

Выполните команду для перевода:

slang gpt --project=<csproj> --target=<culture> --api-key=<api-key>

Где:

  • <csproj> — путь к вашему проекту .NET. Если параметр --project не задан, будет использоваться первый найденный csproj файл проекта в текущей директории.

  • <api-key> — ваш API-ключ OpenAI.

  • <culture> — двухбуквенный код культуры, перевод на которую вы хотите получить. Если --target не задан, будут использоваться культуры уже существующих файлов локализации.

Чтобы выполнить полный перевод базовой локали с перезаписью текущих локалей (если файлы целевых локалей уже существуют), укажите аргумент --full.

Для исключения отдельных ключей из перевода используйте модификатор ignoreGpt:

{
  "key1": "This will be translated",
  "key2(ignoreGpt)": {
    "key3": "This will be ignored"
  }
}

Список поддерживаемых моделей которые можно указать в slang.json следующий:

Модель

Контекст (токены)

Стоимость за 1k входящих токенов

Стоимость за 1k исходящих токенов

gpt-3.5-turbo

4096

$0.0005

$0.0015

gpt-3.5-turbo-16k

16384

$0.003

$0.004

gpt-4

8192

$0.03

$0.06

gpt-4-turbo

64000

$0.01

$0.03

gpt-4o

128000

$0.005

$0.015

gpt-4o-mini

128000

$0.00015

$0.0006

Примечание: 1k токенов ≈ 750 слов на английском языке.

Преимущества использования GPT:

  • Скорость: Автоматический перевод большого количества строк за короткое время

  • Наличие контекста: GPT-модели учитывают контекст, что повышает качество перевода

  • Экономия ресурсов: Сокращение затрат на ручной перевод

Интеграция с различными платформами

Slang.Net не зависит от конкретной платформы и может использоваться в:

  •  Консольных приложениях;

  •  WPF-приложениях;

  •  ASP.NET Core приложениях (включая фронтенд: Blazor, MVC);

  •  Xamarin и MAUI приложениях.

Локализация Web Api

Для установки поддерживаемых культур можно воспользоваться свойством SupportedCultures и BaseCulture:

using Slang.WebApi.i18n;

var builder = WebApplication.CreateBuilder(args);

// Configure supported cultures
builder.Services.Configure<RequestLocalizationOptions>(options =>
{
    string[] supportedCultures = Strings.SupportedCultures.Select(c => c.ToString()).ToArray();

    options.SetDefaultCulture(Strings.BaseCulture.ToString()) 
        .AddSupportedCultures(supportedCultures)    
        .AddSupportedUICultures(supportedCultures); 
});

Осталось подключить middleware, который переключает культуру в зависимости от значения в хедере Accept-Language.

app.UseRequestLocalization();

Локализация AvaloniaUI

Рекомендуется устанавливать строки в XAML через Bindings для переключения локализации в рантайме (без необходимости перезапускать приложение).

<MenuItem  Header="{Binding Root.Screen.Locale1, Source={x:Static localization:Strings.Instance}}" />

Instance поддерживает интерфейс INotifyPropertyChanged для уведомления UI о изменении свойства Root.
Для переключения языка можно воспользоваться методом SetCulture

Для использования локали с параметрами через xaml нужно использовать MultiValueConverter, передавая в параметры Instance и параметры локали.

Например, локаль с параметрами задана следующим образом:

"Pages": {
    "Explorer": {
      "SelectedItems": {
        "one": "Выбран: {n}",
        "other": "Выбрано: {n}"
      },
    }
  },

Тогда ее применение в xaml будет выглядеть следующим образом:

<TextBlock >
  <TextBlock.Text>
    <MultiBinding Converter="{converters:SelectedCountConverter}">
      <Binding Path="Root" Source="{x:Static localization:Strings.Instance}"/>
      <Binding Path="SelectedCount"/>
    </MultiBinding>
  </TextBlock.Text>
</TextBlock>

ГдеSelectedCount - свойство ViewModel.

Конвертер будет выглядеть следующим образом:

public class SelectedCountConverter : IMultiValueConverter
{
    public object? Convert(IList<object?> values, Type targetType, object? parameter, CultureInfo culture)
    {
        if (values.Count == 2)
        {
            if (values[0] is Strings loc)
            {
                if (values[1] is int count)
                    return loc.Pages.Explorer.SelectedItems(count);
            }
        }

        return null;
    }
}

В последних версиях C# (с 11 версии) данный код можно упростить до:

public class SelectedCountConverter : IMultiValueConverter
{
    public object? Convert(IList<object?> values, Type targetType, object? parameter, CultureInfo culture)
    {
        return values is [Strings loc, int count] ? loc.Pages.Explorer.SelectedItems(count) : null;
    }
}

Таким образом, при смене языка через метод Strings.SetCulture, текст автоматически поменяется в интерфейсе на текст выбранной культуры. Именно поэтому не рекомендуется использовать тексты локализации внутри ViewModel, только если это не текстовки каких-либо диалоговых окон, которые обычно не отображаются в момент смены языка.

Заключение

Slang.Net — инструмент для локализации .NET-приложений, который:

  • Упрощает работу с переводами;

  • Делает код чище и понятнее;

  • Позволяет автоматически переводить строки на разные языки благодаря интеграции с GPT;

  • Экономит время и ресурсы.

Если вы ищете решение для поддержки нескольких языков в вашем приложении, Slang.Net — отличный выбор. В статье я кратко описал возможности библиотеки. Дополнительные параметры и флаги вы найдете в файле документации репозитория, где также можно посмотреть примеры их использования.

Буду рад вашим комментариям и предложениям по доработке функционала, API методов, а также новым issue, в случае появления багов или пожеланий. Библиотека молодая, но как порт оригинальной версии на Dart, она уже включает в себя опыт исправления типовых багов, выявленных ранее.

Полезные ссылки

Комментарии (0)