Pull to refresh

Автоматизация локализации в Silverlight при помощи макросов Visual Studio

Reading time6 min
Views1.3K
большая красная кнопка

Здравствуйте. Хочу поделиться небольшой наработкой в области автоматизации локализации приложений разработанных с использованием технологии SilverLight. Прочитав этот пост (Локализация в Silverlight), стало ясно, что придется выносить все строковые константы в ресурсные файлы, что нельзя назвать особо интеллектуальной работой. Поэтому решил пойти длинным путём и попробовать автоматизировать данный процесс с помощью встроенных в Visual Studio макросов.

В связи с тем, что от меня так же требовали немного причесать код, было решено отказаться от полной автоматизации (полного сканирования всех xaml файлов с поиском русских букв и вынесением в ресурсы). Плюс не смог придумать нормальной автоматизации замены строковых констант в .cs файлах.

После анализа были выработаны следующие требования к макросу:
  • проанализировать выделенную пользователем фразу (по нажатию на определённую комбинацию клавиш);
  • найти перевод фразы в google translate (чтобы не напрягать пользователя придумыванием названия ресурса);
  • добавить фразу в указанные заранее ресурсы (причем необходимо, чтобы изменения производились через VS, так как нам нужен сгенерированный прокси класс);
  • заменить выделенную пользователем фразу на биндинг к ресурсу.


В Visual Studio встроен удобный редактор макросов. Чтобы его вызвать необходимо выбрать в меню Tools/Macros/Macros IDE. Перед началом работы, необходимо создать файл с макросами (код пишется на VB .Net). Любая public процедура без параметров — видна в списке макросов и её можно вызывать при нажатии на горячую клавишу, либо через Macro Explorer.

Задача 1. Определение выделенного слова в VS

Dim doc As Document = DTE.ActiveDocument
Dim d As TextDocument = doc.Object
Dim bp = d.Selection.AnchorPoint.CreateEditPoint, ep = d.Selection.BottomPoint.CreateEditPoint
Dim caption As String = bp.GetText(ep)
If (String.IsNullOrWhiteSpace(caption)) Then Exit Sub ' если у нас ничего не выделено - то выходим


Получаем активный документ, приводим его к текстовому документу (более удобный класс, для работы с текстом). Получаем две точки: начало и конец выделения (можно было бы проще, через doc.Selection.Text, но при открытии нового файла в студии и возвращении обратно в редактируемому файлу — сбивается выделение, что очень неудобно, а эти точки нам помогут восстановить выделение текста).

Задача 2. Получение имени ресурса

Наиболее простым способом получения имени ресурса — является перевод выделенной фразы на английский язык (примечание: наиболее простой способ — не является наиболее правильным). Для перевода было решено использовать сервис Google Translate, так как этот сервис имеет простое API
В результате запроса получаем следующий JSON объект:
{"responseData": {"translatedText":"Organization Structure"}, "responseDetails": null, "responseStatus": 200}

Private Function ConvertToResourceName(ByVal text As String) As String
Dim result As String = (New WebClient()).DownloadString("http://ajax.googleapis.com/ajax/services/language/translate?v=1.0&q=" + text + "&langpair=ru|en")
Dim stream As New MemoryStream(Encoding.Unicode.GetBytes(result))
Dim serializer As New System.Runtime.Serialization.Json.DataContractJsonSerializer(GetType(TranslateResult))
Return String.Join("", CType(serializer.ReadObject(stream), TranslateResult).responseData.translatedText _
                                .Split(" ", ".", ",", "!", "(", ")", vbCrLf, vbCr) _
                                .Where(Function(word) Not String.IsNullOrWhiteSpace(word)) _
                                .Select(Function(word) word(0).ToString().ToUpper() + word.Substring(1)) _
                                .ToArray())
End Function

Для получения перевода необходимо выполнить специальный запрос к сервису гугла и передать ему строку с текстом. Единственное, что довольно сильно усложнило данный код, так это то, что в результате нам возвращается не просто строка, а JSON объект, который необходимо предварительно десериализовать. Для десериализации использовался класс DataContractJsonSerializer. Для использования DataContractJsonSerializer в макросах, необходимо добавить сборку System.Runtime.Serialization.dll в references и реализовать класс, соответствующий ответу Гугла, точнее два класса.

Примечание: перед использованием имени, можно выдать пользователю запрос на подтверждение правильности выбора перевода (с возможностью корректировки).

Примечание: для работы LINQ to Object, необходимо добавить references на System.Core.dll

<DataContract()> _
Public Class TranslateResult
    <DataContract()> _
    Public Class ResponseDataClass
        <DataMember()> _
        Public translatedText As String
        <DataMember()> _
        Public responseDetails As String
        <DataMember()> _
        Public responseStatus As String
    End Class
    <DataMember()> _
    Public responseData As ResponseDataClass
End Class

После получения перевода, необходимо немного украсить полученный результат, а именно: удалить все запрещенные символы и привести к CamelCase стилю.

Задача 3. Добавление новой строки в ресурсы

Первоначально необходимо активировать окно с редактором ресурса, либо открыть новое окно в VS. Причем если было открыто новое окно, то фокус переместится на него. Чтобы не сбивать пользователя, необходимо вернуть фокус обратно.
Private Function FindWindow(ByVal fileName As String) As Window
    For i As Integer = 1 To DTE.Windows.Count
        If Not (DTE.Windows.Item(i).ProjectItem Is Nothing) Then
            If (DTE.Windows.Item(i).ProjectItem.FileNames(0) = fileName) Then
                Return DTE.Windows.Item(i)
            End If
        End If
    Next
    Dim oldactive As Document = DTE.ActiveDocument
    Dim newf = DTE.ItemOperations.OpenFile(fileName, Constants.vsViewKindTextView)
    oldactive.Activate()
    Return newf
End Function

Так как файлов ресурсов может быть несколько для разных языков, то запоминаем их в массиве.
Dim resourceFiles() As String = {"полный путь\ProjectResources.resx", _
                                 "полный путь\ProjectResources.kk-KZ.resx"}

Можно оптимизировать данный кусок и попробовать найти автоматически файлы с ресурсами.
Далее пробегаемся по каждому файлу ресурсов, открываем его, проверяем наличие ресурса с таким именем, и если его нет, то добавляем.
Dim paramName As String = ConvertToResourceName(caption)
For Each item As String In resourceFiles
    Dim textDocument As TextDocument = FindWindow(item).Document.Object
    Dim startPoint = textDocument.StartPoint.CreateEditPoint, endPoint = textDocument.EndPoint.CreateEditPoint

    Dim root = XElement.Parse(startPoint.GetText(endPoint))

    Dim param As XElement = root.Elements("data").FirstOrDefault(Function(el) el.Attribute("name") = paramName)
    If (param Is Nothing) Then
        root.Add(New XElement("data", New XAttribute("name", paramName) _
                    , New XAttribute(XName.Get("space", "http://www.w3.org/XML/1998/namespace"), "preserve") _
                    , New XElement("value", caption)))

        startPoint.ReplaceText(endPoint, root.ToString(), vsEPReplaceTextOptions.vsEPReplaceTextAutoformat)
        textDocument.Parent.Save()
    End If
Next

Файл ресурсов представляет собой простую xml-ку с полями data, соответственно наиболее простой способ напрямую модифицировать всё содержимое файла (содержимое получается из открытого документа) и потом целиком обновить весь документ.
После модификации документа — необходимо сохранить изменения, что приведёт к генерации прокси класса.
Примечание: для работы XElement'а необходимо добавить references на System.Xml, System.Xml.Linq;

Задача 4. Заменить выделенное пользователем слово на шаблон биндинга к новому ресурсу

В связи с тем, что обращение к ресурсам в xaml/cs/vb файлах — различаются, то необходимо проверить расширение открытого файла и в соответствии с этим формировать строку.
Dim replaceString As String

Select Case Path.GetExtension(doc.FullName).ToLower()
    Case ".xaml" : replaceString = "{Binding Source={StaticResource ResourceProvider},Path=ProjectResources." + paramName + "}"
    Case ".cs" : replaceString = "ProjectResources." + paramName
    Case Else : replaceString = paramName
End Select

bp.ReplaceText(ep, replaceString, 0) ' заменяем текст

d.Selection.MoveToPoint(bp)
d.Selection.SwapAnchor()
d.Selection.MoveToPoint(ep, True) ' выделить от предыдущей точки, то текущей


Все макрос готов к использованию.

Преимущества/недостатки использования макросов

  • вызов макроса можно повесить на горячие клавиши, через пункт меню tools/options в разделе Keyboard;
  • очень легко исправлять код, в случае, если необходимо его немного доработать (например, можно не писать сложный механизм для поиска файлов с ресурсами, а просто завести константные пути в коде и при необходимости подправлять их);
  • довольно легкая отладка кода;
  • хорошая интеграция с VS.

из явных недостатков
  • нельзя писать код на C#;
  • низкая производительность, довольно долго стартуют;
  • частые вылеты, особенно во время отладки.

В статье приведён общий подход к автоматизации формирования ресурсных файлов, соответственно вы можете его корректировать как вам необходимо.
Tags:
Hubs:
Total votes 12: ↑8 and ↓4+4
Comments1

Articles