In this previous post we introduced a technique for automatically translating AutoCAD’s tooltips into one of 35 different languages via an online translation service.
To improve the process at various levels – rendering it more efficient and enabling the possibility of local editing and crowdsourced localization – this post introduces caching of the translation results to a set of local XML files.
A few comments on the implementation changes:
- There’s now a TRANSTIPSSRC command, which allows you to set the source language (using a similar UI to the target language). This is useful if you’re working on non-English AutoCAD (our Localization team is already looking at it, to see whether it helps them validate the correctness of product translations into other languages, which is interesting).
- I don’t ultimately see either the source or target language as being options the user should ultimately set: the source language should be based on the product’s language, while the target should – in most cases – be based on the OS language (thanks to Cyrille Fauvel for that suggestion :-).
- The approach we use for unresolved tooltips has been adjusted: we now set the cursor to the top-center of the primary screen (getting it well clear of the item currently hovered over, but also away from the top-left of the screen, which has a tendency to make the big red A flash when AutoCAD is maximised).
- A few adjustments have been made to the creation of a unique ID for caching purposes, as well as the places we mark these IDs as having been translated.
- The main change, of course, is to save the translations returned from the server into XML files. A single XML file will contain the various translations of the source content for a particular item. This change enables some very important capabilities:
- The implementation is now more efficient: we don’t have to call out to the web service when we have pre-translated content for a target language.
- Approved translations can be deployed with the app.
- An editing/approval mechanism could easily be implemented to enable crowdsourced localization of items.
- Caching the source content allows us to switch back to the source language but also to translate into other target languages (before this you had to restart AutoCAD).
Here’s the updated C# code:
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
using Autodesk.Windows;
using System.Runtime.Serialization;
using System.Collections.Generic;
using System.Windows.Documents;
using System.Windows.Controls;
using System.Windows;
using System.Text.RegularExpressions;
using System.Text;
using System.Linq;
using System.Net;
using System.Xml;
using System.IO;
using System;
[assembly: ExtensionApplication(typeof(TranslateTooltips.Commands))]
namespace TranslateTooltips
{
public class Commands : IExtensionApplication
{
// Keep track of currently translated items
static List<string> _handled = null;
// Our source and target languages
static string _srcLang = "en";
static string _trgLang = "";
// Location off the main application to which to move
// the cursor, for tooltip redisplay
static System.Drawing.Point _offTarget;
public void Initialize()
{
HijackTooltips();
}
public void Terminate()
{
}
[CommandMethod("ADNPLUGINS", "TRANSTIPSSRC", CommandFlags.Modal)]
public static void ChooseSourceLanguage()
{
Document doc =
Autodesk.AutoCAD.ApplicationServices.Application.
DocumentManager.MdiActiveDocument;
Editor ed = doc.Editor;
// Get the list of language codes and their corresponding
// names
List<string> codes = GetLanguageCodes();
string[] names = GetLanguageNames(codes);
// Make sure we have as many names as languages supported
if (codes.Count == names.Length)
{
// Ask the user to select a source language
string lang =
ChooseLanguage(ed, codes, names, _srcLang, true);
// If the language code returned is neither empty
// nor the same as the target, print the code's
// name and set it as the new target
if (lang == _trgLang)
{
ed.WriteMessage(
"\nSource language cannot be the same as the " +
"target language."
);
}
else if (!String.IsNullOrEmpty(lang))
{
// Get the name corresponding to a language code
string name =
names[
codes.FindIndex(0, x => x == lang)
];
// Print it to the user
ed.WriteMessage(
"\nSource language set to {0}.\n", name
);
// Set the new source language
_srcLang = lang;
}
}
}
[CommandMethod("ADNPLUGINS", "TRANSTIPS", CommandFlags.Modal)]
public static void ChooseTranslationLanguage()
{
Document doc =
Autodesk.AutoCAD.ApplicationServices.Application.
DocumentManager.MdiActiveDocument;
Editor ed = doc.Editor;
// Get the list of language codes and their corresponding
// names
List<string> codes = GetLanguageCodes();
string[] names = GetLanguageNames(codes);
// Make sure we have as many names as languages supported
if (codes.Count == names.Length)
{
// Ask the user to select a target language
string lang =
ChooseLanguage(ed, codes, names, _trgLang, false);
// If the language code returned is empty or the same
// as the source, turn off translations
if (lang == "" || lang == _srcLang)
{
ed.WriteMessage(
"\nTooltip translation is turned off."
);
_trgLang = "";
}
else if (lang != null)
{
// Otherwise get the name corresponding to a language
// code
string name =
names[
codes.FindIndex(0, x => x == lang)
];
// Print it to the user
ed.WriteMessage(
"\nTooltips will be translated into {0}.\n", name
);
// Set the new target language
_trgLang = lang;
}
}
}
private static string ChooseLanguage(
Editor ed, List<string> codes, string[] names,
string lang, bool source
)
{
// First option (0) is to unselect
ed.WriteMessage("\n0 None");
// The others (1..n) are the languages
// available on the server
for (int i = 0; i < names.Length; i++)
{
ed.WriteMessage("\n{0} {1}", i + 1, names[i]);
}
// Adjust the prompt based on whether selecting
// a source or target language
PromptIntegerOptions pio =
new PromptIntegerOptions(
String.Format(
"\nEnter number of {0} language to select: ",
source ? "source" : "target"
)
);
// Add each of the codes as hidden keywords, which
// allows the user to also select the language using
// the 2-digit code (good for scripting on startup,
// to avoid having to hard code a number)
foreach (string code in codes)
{
pio.Keywords.Add(code, code, code, false, true);
}
// Set the bounds and the default value
pio.LowerLimit = 0;
pio.UpperLimit = names.Length;
if (codes.Contains(lang))
{
pio.DefaultValue =
codes.FindIndex(0, x => lang == x) + 1;
}
else
{
pio.DefaultValue = 0;
}
pio.UseDefaultValue = true;
// Get the selection
PromptIntegerResult pir = ed.GetInteger(pio);
string resLang = null;
if (pir.Status == PromptStatus.Keyword)
{
// The code was entered as a string
if (!codes.Contains(pir.StringResult))
{
ed.WriteMessage(
"\nNot a valid language code."
);
resLang = null;
}
else
{
resLang = pir.StringResult;
}
}
else if (pir.Status == PromptStatus.OK)
{
// A number was selected
if (pir.Value == 0)
{
// A blank string indicates none
resLang = "";
}
else
{
// Otherwise we return the corresponding
// code
resLang = codes[pir.Value - 1];
}
}
return resLang;
}
public static void HijackTooltips()
{
// For unresolved tooltips we move the cursor off
// the current item and bring it back to force
// redisplay. Here we calculate the target location
// for the cursor as the top center of the primary
// screen (will hopefully avoid flashing for most
// scenarios)
System.Drawing.Rectangle bounds =
System.Windows.Forms.Screen.PrimaryScreen.Bounds;
_offTarget =
new System.Drawing.Point(
bounds.X + (bounds.Width / 2), bounds.Y
);
// Instantiate our list of translated items
if (_handled == null)
{
_handled = new List<string>();
}
// Respond to an event fired when any tooltip is
// displayed inside AutoCAD
Autodesk.Windows.ComponentManager.ToolTipOpened +=
(s, e) =>
{
try
{
if (!String.IsNullOrEmpty(_trgLang))
{
// The outer object is of an Autodesk.Internal
// class, hence subject to change
Autodesk.Internal.Windows.ToolTip tt =
s as Autodesk.Internal.Windows.ToolTip;
if (tt != null)
{
if (tt.Content is RibbonToolTip)
{
// Enhanced tooltips
RibbonToolTip rtt = (RibbonToolTip)tt.Content;
if (rtt.Content == null &&
rtt.ExpandedContent == null)
{
// To stop from closing the browser control
// tooltips in Revit
if (!rtt.Title.Contains(" : "))
{
CloseAndReshowTooltip(tt);
}
}
else
{
rtt.Content =
TranslateIfString(
rtt.Content, GetId(rtt)
);
TranslateObjectContent(
rtt.Content, GetId(rtt)
);
// Translate any expanded content
// (adding a suffix to the ID to
// distinguish from the basic content)
rtt.ExpandedContent =
TranslateIfString(
rtt.ExpandedContent,
GetExpandedId(GetId(rtt))
);
TranslateObjectContent(
rtt.ExpandedContent,
GetExpandedId(GetId(rtt))
);
// Force display of the tooltip, which avoids it
// not being shown for certain controls
// (currently hard-coding the offset, which may
// be based on the height of the cursor's glyph)
tt.Show(
System.Windows.Forms.Cursor.Position.X,
System.Windows.Forms.Cursor.Position.Y + 16
);
}
}
else if (tt.Content is UriKey)
{
// This is called once for tooltips that
// need to be resolved by the system
// Here we close the tooltip and defer the
// redisplay to the system
CloseAndReshowTooltip(tt);
}
else
{
// A basic, string-only tooltip
tt.Content = TranslateIfString(tt.Content, null);
}
}
}
}
// Indiscriminate catch, ultimately to avoid crashing host
catch { }
};
}
private static void CloseAndReshowTooltip(
Autodesk.Internal.Windows.ToolTip tt
)
{
// Close the current (as yet unresolved) tooltip
tt.Close();
// Store the current cursor location
System.Drawing.Point pt =
System.Windows.Forms.Cursor.Position;
// Set the new cursor location to that previously
// calculated (the top-center of the primary display,
// which should avoid strange activity inside the host
// product)
System.Windows.Forms.Cursor.Position = _offTarget;
// Process events (several times, for luck)
for (int i = 0; i < 5; i++)
{
System.Windows.Forms.Application.DoEvents();
}
// Set the cursor back to display the resolved
// (and now translated) tooltip
System.Windows.Forms.Cursor.Position = pt;
}
private static object TranslateIfString(
object obj, string id
)
{
// If the object passed in is a string,
// return its translation to the caller
object ret = obj;
if (obj is string)
{
string trans =
TranslateContent((string)obj, id);
if (!String.IsNullOrEmpty(trans))
{
ret = trans;
}
}
return ret;
}
private static void TranslateObjectContent(
object obj, string id
)
{
// Translate more complex objects and their
// contents
if (obj != null)
{
if (obj is TextBlock)
{
// Translate TextBlocks
TextBlock tb = (TextBlock)obj;
TranslateTextBlock(tb, id);
}
else if (obj is StackPanel)
{
// And also handle StackPanels of content
StackPanel sp = (StackPanel)obj;
TranslateStackPanel(sp, id);
}
}
}
private static void TranslateTextBlock(
TextBlock tb, string id
)
{
// Translate a TextBlock
string trans =
TranslateContent(tb.Text, id);
if (!String.IsNullOrEmpty(trans))
{
tb.Text = trans;
}
}
private static void TranslateStackPanel(
StackPanel sp, string id
)
{
// Translate a StackPanel of content
TextBlock tb;
for (int i=0; i < sp.Children.Count; i++)
{
UIElement elem = sp.Children[i];
tb = elem as TextBlock;
if (tb != null)
{
TranslateTextBlock(tb, GetExpandedItemId(id, i));
}
else
{
FlowDocumentScrollViewer sv =
elem as FlowDocumentScrollViewer;
if (sv != null)
{
TranslateFlowDocumentScrollViewer(
sv, GetExpandedItemId(id, i)
);
}
}
}
}
private static void TranslateFlowDocumentScrollViewer(
FlowDocumentScrollViewer sv, string id
)
{
// Translate a FlowDocumentScrollViewer, which
// hosts content such as bullet-lists in
// certain tooltips (e.g. for HATCH)
int n = 0;
Block b = sv.Document.Blocks.FirstBlock;
while (b != null)
{
List l = b as List;
if (l != null)
{
ListItem li = l.ListItems.FirstListItem;
while (li != null)
{
Block b2 = li.Blocks.FirstBlock;
while (b2 != null)
{
Paragraph p = b2 as Paragraph;
if (p != null)
{
Inline i = p.Inlines.FirstInline;
while (i != null)
{
string contents =
i.ContentStart.GetTextInRun(
LogicalDirection.Forward
);
// We need to suffix the IDs to
// keep them distinct
string trans =
TranslateContent(
contents, GetExpandedItemId(id, n)
);
if (!String.IsNullOrEmpty(trans))
{
i.ContentStart.DeleteTextInRun(
contents.Length
);
i.ContentStart.InsertTextInRun(trans);
}
n++;
i = i.NextInline;
}
}
b2 = b2.NextBlock;
}
li = li.NextListItem;
}
}
b = b.NextBlock;
}
}
private static string GetId(RibbonToolTip rtt)
{
// The ID should be the command string, where
// it exists (e.g. AutoCAD) or the item title,
// otherwise (e.g. Revit)
return
String.IsNullOrEmpty(rtt.Command) ?
rtt.Title :
rtt.Command;
}
private static string GetExpandedId(string id)
{
// Suffix any non-null ID with -x for expanded content
return String.IsNullOrEmpty(id) ? id : id + "-x";
}
private static string GetExpandedItemId(string id, int n)
{
// Get an ID for a sub-item of expanded content
return
String.IsNullOrEmpty(id) ? id : id + "_" + n.ToString();
}
private static void MarkAsTranslated(string id)
{
// Mark an item as having been translated
if (!String.IsNullOrEmpty(id) && !_handled.Contains(id))
_handled.Add(id);
}
private static void UnmarkAsTranslated(string id)
{
// Remove an item from the list of marked items
if (!String.IsNullOrEmpty(id) && _handled.Contains(id))
_handled.Remove(id);
}
private static bool AlreadyTranslated(string id)
{
// Check the list, to see whether an item has been
// translated
return _handled.Contains(id);
}
private static string TranslateContent(
string contents, string id
)
{
// Our translation to return, and the string to pass
// as source (which may be the string passed in,
// or may come from disk)
string trans = null,
source = contents;
// If the target language is empty, we'll take the source
// language
string trgLang =
String.IsNullOrEmpty(_trgLang) ? _srcLang : _trgLang;
// Get the name of the XML file for this data
string fn = GetXmlFileName(id);
if (File.Exists(fn))
{
// If the item has already been translated in the UI,
// we need to load the source language version to
// retranslate that
if (AlreadyTranslated(id))
{
source = LoadXmlTranslation(fn, _srcLang);
source = (source == null ? contents : source);
// If the source and target are the same,
// reset to the source language
if (_srcLang == trgLang)
{
UnmarkAsTranslated(id);
return source;
}
}
// Attempt to get a prior translation from XML
trans = LoadXmlTranslation(fn, trgLang);
}
if (trans == null)
{
// If there was no translation on disk, translate
// via the online service
trans =
GetTranslatedText(_srcLang, trgLang, source);
// If the filename is valid, save the data to XML
if (!String.IsNullOrEmpty(fn))
{
SaveXmlTranslation(
fn, _srcLang, source, trgLang, trans
);
}
}
if (!String.IsNullOrEmpty(trans))
{
MarkAsTranslated(id);
}
return trans;
}
private static string GetXmlFileName(string id)
{
// The XML file will be beneath My Documents, under
// a sub-folder named "TransTip Cache"
string path =
System.Environment.GetFolderPath(
Environment.SpecialFolder.MyDocuments
) + "\\TransTips Cache";
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
}
// The filename is the id with the .xml extension
return
id == null ?
"" :
path + "\\" + MakeValidFileName(id) + ".xml";
}
private static string MakeValidFileName(string name)
{
string invChars =
Regex.Escape(new string(Path.GetInvalidFileNameChars()));
string invRegEx = string.Format(@"[{0}]", invChars + ".");
return Regex.Replace(name, invRegEx, "-");
}
private static void SaveXmlTranslation(
string fn,
string srcLang, string contents,
string trgLang, string trans
)
{
if (File.Exists(fn))
{
// Add our content to an existing XML file
XmlDocument xd = new XmlDocument();
xd.Load(fn);
XmlNode tg =
xd.SelectSingleNode(
"/Translation/Content[@Language='" + trgLang + "']"
);
if (tg == null)
{
XmlNode xn =
xd.CreateNode(XmlNodeType.Element, "Content", "");
XmlAttribute xlang = xd.CreateAttribute("Language");
xlang.Value = trgLang;
xn.InnerText = trans;
xn.Attributes.Append(xlang);
xd.GetElementsByTagName("Translation")[0].InsertAfter(
xn,
xd.GetElementsByTagName("Translation")[0].LastChild
);
xd.Save(fn);
}
}
else
{
// Create a new Unicode XML file
XmlTextWriter xw =
new XmlTextWriter(fn, Encoding.Unicode);
using (xw)
{
xw.WriteStartDocument();
xw.WriteStartElement("Translation");
xw.WriteStartElement("Content");
xw.WriteAttributeString("Language", srcLang);
xw.WriteAttributeString("Source", "True");
xw.WriteString(contents);
xw.WriteEndElement();
xw.WriteStartElement("Content");
xw.WriteAttributeString("Language", trgLang);
xw.WriteString(trans);
xw.WriteEndElement();
xw.WriteEndElement();
xw.Close();
}
}
}
private static string LoadXmlTranslation(
string fn, string lang
)
{
// Load the XML document
XmlDocument xd = new XmlDocument();
xd.Load(fn);
// Look for a Content node for our language
XmlNode tg =
xd.SelectSingleNode(
"/Translation/Content[@Language='" + lang + "']"
);
return tg == null ? null : tg.InnerXml;
}
// Replace the following string with the AppId you receive
// from the Bing Developer Center
const string AppId =
"Kean's Application Id – please get your own :-)";
const string baseTransUrl =
"http://api.microsofttranslator.com/v2/Http.svc/";
private static string GetTranslatedText(
string from, string to, string content
)
{
// Translate a string from one language to another
string uri =
baseTransUrl + "Translate?appId=" + AppId +
"&text=" + content + "&from=" + from + "&to=" + to;
// Create the request
HttpWebRequest request =
(HttpWebRequest)WebRequest.Create(uri);
string output = null;
WebResponse response = null;
try
{
// Get the response
response = request.GetResponse();
Stream strm = response.GetResponseStream();
// Extract the results string
DataContractSerializer dcs =
new DataContractSerializer(
Type.GetType("System.String")
);
output = (string)dcs.ReadObject(strm);
}
catch (WebException e)
{
ProcessWebException(
e, "\nFailed to translate text."
);
}
finally
{
if (response != null)
{
response.Close();
response = null;
}
}
return output;
}
private static List<string> GetLanguageCodes()
{
// Get the list of language codes supported
string uri =
baseTransUrl + "GetLanguagesForTranslate?appId=" + AppId;
// Create the request
HttpWebRequest request =
(HttpWebRequest)WebRequest.Create(uri);
WebResponse response = null;
List<String> codes = null;
try
{
// Get the response
response = request.GetResponse();
using (Stream stream = response.GetResponseStream())
{
// Extract the list of language codes
DataContractSerializer dcs =
new DataContractSerializer(typeof(List<String>));
codes = (List<String>)dcs.ReadObject(stream);
}
}
catch (WebException e)
{
ProcessWebException(
e, "\nFailed to get target translation languages."
);
}
finally
{
if (response != null)
{
response.Close();
response = null;
}
}
return codes;
}
public static string[] GetLanguageNames(List<string> codes)
{
string uri =
baseTransUrl + "GetLanguageNames?appId=" + AppId +
"&locale=en";
// Create the request
HttpWebRequest req =
(HttpWebRequest)WebRequest.Create(uri);
req.ContentType = "text/xml";
req.Method = "POST";
// Encode the list of language codes
DataContractSerializer dcs =
new DataContractSerializer(
Type.GetType("System.String[]")
);
using (Stream stream = req.GetRequestStream())
{
dcs.WriteObject(stream, codes.ToArray());
}
WebResponse response = null;
try
{
// Get the response
response = req.GetResponse();
using (Stream stream = response.GetResponseStream())
{
// Extract the list of language names
string[] results = (string[])dcs.ReadObject(stream);
string[] names =
results.Select(x => x.ToString()).ToArray();
return names;
}
}
catch (WebException e)
{
ProcessWebException(
e, "\nFailed to get target language."
);
}
finally
{
if (response != null)
{
response.Close();
response = null;
}
}
return null;
}
private static void ProcessWebException(
WebException e, string message
)
{
// Provide information regarding an exception
Document doc =
Autodesk.AutoCAD.ApplicationServices.Application.
DocumentManager.MdiActiveDocument;
Editor ed = doc.Editor;
ed.WriteMessage("{0}: {1}", message, e.ToString());
// Obtain detailed error information
string strResponse = string.Empty;
using (
HttpWebResponse response =
(HttpWebResponse)e.Response
)
{
using (
Stream responseStream =
response.GetResponseStream()
)
{
using (
StreamReader sr =
new StreamReader(
responseStream, System.Text.Encoding.ASCII
)
)
{
strResponse = sr.ReadToEnd();
}
}
}
// Print it to the user
ed.WriteMessage(
"\nHttp status code={0}, error message={1}",
e.Status, strResponse
);
}
}
}
The behaviour is more-or-less the same, although the TRANSTIP command will now more effectively switch between languages. Here’s the same tooltip in a number of languages, all in the same editing session (after subsequent uses of the TRANSTIP command).
The crowdsourced editing mechanism – while enabled by saving to XML – has yet to be implemented via a direct, in-product UI. For now, though, the XML files could very easily be edited and deployed separately by someone choosing to deploy a specific translation of AutoCAD’s tooltips.
For instance, here's the content of the SCALE.xml file from the cache, containing the results of that items text having been translated into all 35 languages:
<?xml version="1.0" encoding="utf-16"?>
<Translation>
<Content Source="True" Language="en">
Enlarges or reduces selected objects, keeping the proportions
of the object the same after scaling
</Content>
<Content Language="fr">
Agrandit ou réduit les objets sélectionnés, en conservant les
proportions de l'objet de la même après la mise à l'échelle
</Content>
<Content Language="ar">
تكبير أو يقلل من
الكائنات المحددة، الحفاظ على أبعاد الكائن نفسه بعد رفع
</Content>
<Content Language="bg">
Увеличава или намалява избраните обекти, като пропорциите на
обекта, същото след мащабиране
</Content>
<Content Language="ca">
Augmenta o redueix els objectes seleccionats, mantenir les
proporcions de l'objecte el mateix després de l'ampliació
</Content>
<Content Language="zh-CHS">
放大或缩小所选的对象,同样保持对象的比例后缩放
</Content>
<Content Language="zh-CHT">
放大或縮小所選的物件,同樣保持物件的比例後縮放
</Content>
<Content Language="cs">
Zvětší nebo zmenší vybraných objektů, udržuje proporce
objektu stejná po změně velikosti
</Content>
<Content Language="da">
Forstørrer eller formindsker markerede objekter, holde
objektet proportioner det samme efter skalering
</Content>
<Content Language="nl">
Vergroot of verkleint u geselecteerde objecten, houden
de verhoudingen van het object hetzelfde na schalen
</Content>
<Content Language="et">
Suurendab või vähendab valitud objektid, hoides objekti
proportsioonide sama pärast mastaapimine
</Content>
<Content Language="fi">
Suurentaa tai pienentää valittuja objekteja, objektin
mittasuhteita sama pitäminen jälkeen skaalaus
</Content>
<Content Language="de">
Vergrößert oder verkleinert die ausgewählten Objekte,
halten den Proportionen des Objekts gleich nach Skalierung
</Content>
<Content Language="el">
Μεγεθύνει ή μειώνει τα επιλεγμένα αντικείμενα, διατηρώντας
τις αναλογίες του αντικειμένου το ίδιο μετά την κλιμάκωση
</Content>
<Content Language="ht">
Agrandir ou réduit sélectionné objets, kenbe proportions de
objet a menm bagay la tou apre dekale
</Content>
<Content Language="he">
הגדלה או הקטנה של אובייקטים שנבחרו, שמירה
על הפרופורציות של האובייקט אותו לאחר שינוי קנה מידה
</Content>
<Content Language="hu">
Nagyítása vagy kicsinyítése a kijelölt objektumok, tartás
az objektum arányait azonos után méretezés
</Content>
<Content Language="id">
Membesar atau mengurangi objek terpilih, menjaga proporsi
objek yang sama setelah scaling
</Content>
<Content Language="it">
Ingrandisce o riduce gli oggetti selezionati, mantenendo le
proporzioni dell'oggetto la stessa dopo aver scalatura
</Content>
<Content Language="ja">
拡大または縮小し、オブジェクトの縦横比、同じスケーリング後維持、
選択したオブジェクト
</Content>
<Content Language="ko">
확대 또는 축소 배율 조정 후 동일 개체의 비율 유지 선택한 개체
</Content>
<Content Language="lv">
Paplašina vai samazina atlasītos objektus, saglabājot objekta
proporcijas pats pēc mērogošanas
</Content>
<Content Language="lt">
Padidina arba sumažina pažymėtus objektus, išlaikyti objekto
proporcijas tą patį po mastelio nustatymas
</Content>
<Content Language="no">
Forstørrer eller reduserer markerte objekter, beholde
proporsjonene for objektet det samme etter skalering
</Content>
<Content Language="pl">
Powiększa lub zmniejsza zaznaczonych obiektów, utrzymując
proporcje obiektu to samo po skalowanie
</Content>
<Content Language="pt">
Amplia ou reduz a objetos selecionados, mantendo as
proporções do objeto o mesmo depois de dimensionamento
</Content>
<Content Language="ro">
Măreşte sau reduce obiectele selectate, păstrând proporţiile
obiectului la fel după scalare
</Content>
<Content Language="ru">
Увеличивает или уменьшает выбранных объектов, изменяя
пропорций объекта после масштабирования
</Content>
<Content Language="sk">
Zväčší alebo zmenší vybraté objekty, vedenie proporcie
objektu rovnaké po mierka
</Content>
<Content Language="sl">
Poveča oziroma zmanjša izbrane predmete, vodenje razmerja
predmeta, enaki po škaje
</Content>
<Content Language="es">
Aumenta o reduce los objetos seleccionados, mantener las
proporciones del objeto de la misma después de escalar
</Content>
<Content Language="sv">
Förstorar eller förminskar markerade objekt, hålla objektet
proportioner samma efter skalning
</Content>
<Content Language="th">
ขยาย หรือวัตถุที่เลือก การรักษาสัดส่วนของวัตถุเดียวกันหลังจากปรับมาตราส่วน
</Content>
<Content Language="tr">
Büyütür veya küçültür seçili nesneler, nesnenin oranlarını
aynı tutmak sonra ölçekleme
</Content>
<Content Language="uk">
Збільшує або зменшує виділених об'єктів, зберігаючи
пропорції об'єкта ж після масштабування
</Content>
<Content Language="vi">
Enlarges hoặc làm giảm đối tượng được chọn, giữ tỷ lệ của
các đối tượng như vậy sau khi rộng
</Content>
</Translation>
It may prove better to keep separate files for each language, and that’s certainly something that can be implemented with little effort, should the need arise.
I think that’s enough, for today. My next steps with this tool will be to create a common implementation that can be used in other Autodesk products making use of AdWindows.dll (Jeremy Tammik very kindly ported the code to Revit, so we at least know it’s possible). I’ll probably stick with a “shared source” approach, rather than adding the overhead of a standalone DLL, but – either way – some work will be needed to refactor the appropriate code into a separate, shared implementation.
Ah yes, and I should probably make sure the application behaves predictably – or at least fails gracefully – when an Internet connection is not available.