<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>my tech blog &#187; Software</title>
	<atom:link href="http://billauer.se/blog/category/software/feed/" rel="self" type="application/rss+xml" />
	<link>https://billauer.se/blog</link>
	<description>Anything I found worthy to write down.</description>
	<lastBuildDate>Thu, 12 Mar 2026 11:36:00 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.1.2</generator>
		<item>
		<title>Messy jots on AppSheets</title>
		<link>https://billauer.se/blog/2026/01/jots-on-appsheets/</link>
		<comments>https://billauer.se/blog/2026/01/jots-on-appsheets/#comments</comments>
		<pubDate>Fri, 02 Jan 2026 12:01:44 +0000</pubDate>
		<dc:creator>eli</dc:creator>
				<category><![CDATA[Software]]></category>

		<guid isPermaLink="false">https://billauer.se/blog/?p=7186</guid>
		<description><![CDATA[Introduction These are the jots I wrote down as I learned my way through AppSheet. So this a messy and not so coherent post. Info is scattered here, not necessarily in a sensible order. This is not a tutorial. My interest comes from the fact that I&#8217;m looking for an apartment, and I want to [...]]]></description>
			<content:encoded><![CDATA[<h3>Introduction</h3>
<p>These are the jots I wrote down as I learned my way through AppSheet. So this a messy and not so coherent post. Info is scattered here, not necessarily in a sensible order. <strong>This is not a tutorial.</strong></p>
<p>My interest comes from the fact that I&#8217;m looking for an apartment, and I want to go through a lot of details in each apartment I visit. So I looked for a tool that allows me to organize the information I collect, from the ad, through the thoughts I have about the apartment before visiting it, and most importantly, to efficiently examine the apartment.</p>
<p>This can be done with a plain paper form, of course, but when there are many details and stages, it&#8217;s easy to miss one. So the crucial part of my application is to get a quick final view that ensures me I&#8217;ve done everything, just before thanking for the visit. This is the most important part, and it&#8217;s also the most difficult to implement.</p>
<p>In hindsight, it took 9 days, which is much more time and effort than I intended and wanted to invest on this. I had the &#8220;almost done&#8221; feeling more or less from the beginning, but every next step turned out much more difficult (and annoying) than I imagined. AppSheet makes you feel like a code monkey that doesn&#8217;t know coding. And if AppSheet is used for a long-term project that is maintained over time, be sure to have a QA team that tests everything in every possible combination. The framework invites silly bugs everywhere.</p>
<h3>General</h3>
<ul>
<li>In a nutshell, AppSheet takes data from a database (preferably represented as a spreadsheet in Google Sheets) and presents this data in different <strong>Views</strong>, allowing both displaying and changing the data. The other side of AppSheet is called <strong>Automation</strong>, its ability to generate files, send emails / SMSes, submit HTTP requests etc. with the information in the database, based upon a template. This can be a pdf for an invoice, or a summary report of an inspection. Or an XML or JSON file, pushed to a web server to initiate some action on the information. So Views are primarily intended to feed data into the system, and Automation covers the consumption of the data for a useful purpose.</li>
<li>AppSheet at no cost is unlimited in time and covers every feature of the platform with <a rel="noopener" href="https://about.appsheet.com/pricing/" target="_blank">up to 10 test users</a>. One user, myself, is enough. In free mode, the app can is limited to Prototype status, which has no significance when used only by myself.</li>
<li>AppSheet reminds of Drupal in many ways, and it&#8217;s not necessarily a good thing. Lots of click-click-click around, everything is graphical, and one ends up feeling like a machine doing a silly task a gazillion times because it&#8217;s the same as configuring a Windows Server. Every. Single. Detail. Must. Be. Configured. Manually. With a prayer that there will never be a need to change all those settings that have been repeatedly configured. Plus, small bugs creep in everywhere, as there are a whole lot of details that one must keep track on when making every little change.</li>
<li>Another thing common with Drupal is that every little task takes a lot of time, and sometimes the answer is simply &#8220;no, that&#8217;s impossible&#8221;. At least with Drupal, one can hack the PHP code. For example, the one thing that I considered important, but didn&#8217;t implement, was to get a list of the items in a checklist that I didn&#8217;t check. I didn&#8217;t want to implement the checklist as individual Yes/No (boolean) columns, because adding an item would require modifying the table, regenerating the schema, editing the View and then the report. So I did that with an EnumList, but the only information it emits is what was checked. So all in all, there is no way to implement this rather important feature without turning the checklist into a nightmare to maintain in the long run.</li>
<li>There are certain <a rel="noopener" href="https://support.google.com/appsheet/answer/12653576?hl=en&amp;ref_topic=12632222" target="_blank">limitations</a> on databases and tables, in particular 1000 rows per database in the Free plan and for all plans there are 20 tables per database and 100 columns per table. Don&#8217;t worry, you&#8217;ll lose your sanity before getting even near these limits.</li>
<li>Press the &#8220;Save&#8221; button to the upper right to make changes visible (e.g. when checking with mobile app). This also clears existing warnings, whether they were tended to or not.</li>
<li>Offline use works great: No problem using the mobile phone even with no Intenet coverage. Update to cloud occurs later (may be required to open the app for that). Actually, the only reason I use AppSheets instead of a JavaScript home-cooked application is the ability to work offline.</li>
<li>Backing up an app: Impossible. There&#8217;s no way to download the app&#8217;s own settings, and deleting an app accidentally is easy. The best solution I found for this is to share the app (or better, a copy of it) with another user (being yourself) and make a copy of the app as the other user (note to self: It&#8217;s the &#8220;obscura&#8221; account). And never log in as the other user again. This way, the copy at the other user is safe from accidental tampering. I speculate that the reason for not being able to download the app itself is that AppSheet&#8217;s business model is pay-per-user, and it&#8217;s free for less than 10 users. So had it been possible to download and restore the app from a file, a lot of no-cost single-user accounts would have been created and deployed.</li>
</ul>
<h3>Random insights</h3>
<ul>
<li>Each time &#8220;Save&#8221; is clicked, a new version is saved. It&#8217;s possible to go back to older versions. Version history is saved 7 days back by default.</li>
<li>It&#8217;s possible to run a preview of the app by right-clicking the three dots to the right of the app&#8217;s description in the list of apps. Doing that, <strong>the specific version of the app is previewed</strong>, ignoring subsequent new saved versions of the app. There&#8217;s a misleading &#8220;refresh&#8221; icon on the app preview. It probably only updates data, not the app itself.</li>
<li>If a &#8220;Map&#8221; view is generated automatically, delete it. Otherwise it will show up as the default view for everything (push marketing, it&#8217;s called), and it&#8217;s as annoying as it&#8217;s pointless.</li>
</ul>
<h3>Sources of info</h3>
<ul>
<li><a rel="noopener" href="https://support.google.com/appsheet/table/10104782" target="_blank">List of functions</a> for use in expressions. There isn&#8217;t so many of them.</li>
<li><a rel="noopener" href="https://support.google.com/appsheet/answer/10107946?hl=en" target="_blank">Boolean expression</a> (and that &#8220;not equal&#8221; is &lt;&gt;, not !=).</li>
<li><a rel="noopener" href="https://support.google.com/appsheet/topic/11445504" target="_blank">About Templates</a></li>
<li>About <a rel="noopener" href="https://support.google.com/appsheet/answer/11539253" target="_blank">expressions and built-in variables</a>. For columns that are references, an expression like [colA].[ColB] is used (<a rel="noopener" href="https://support.google.com/appsheet/answer/10107396" target="_blank">official docs</a> about dereferencing).</li>
<li><a rel="noopener" href="https://support.google.com/appsheet/answer/11568425?hl=en" target="_blank">If-expressions in templates</a></li>
</ul>
<h3>Creating a new app</h3>
<p>This describes how to create an app based upon a spreadsheet. The spreadsheet&#8217;s first row consists of the description of each column, and the following rows contain data. Multiple sheets are treated as separate tables, each sheet&#8217;s name being the AppSheet table name (so don&#8217;t rename the sheets in the spreadsheet).</p>
<p>Upload the .ods or .xlsx file to Google Drive, or start a new Google Sheet.</p>
<p>if you&#8217;re already registered with AppSheet, right-click the file in Google Drive, pick Open With and choose AppSheet. An app is generated automatically from the data in the file. The name of the app will be the same as the originating spreadsheet&#8217;s file name. The column types (text, number, price etc.) are automatically assigned, but can be changed later on in the Data section. It&#8217;s even possible to change the type to Image, in which case pictures that are uploaded or taken with the mobile phone are stored in a subdirectory to where the Google Sheet item is saved with a name like appname_Images/. The value in the spreadsheet is the path to the image file, e.g. appname_Images/J1.Silly.140258.jpg.</p>
<p><strong>It&#8217;s best to delete</strong> the .ods or .xlsx from Google drive at this stage to avoid confusion: A Google Sheets entry is created in the same directory. Changes made with AppSheet go to this entry, not to the file used to create the app.</p>
<p>Doing the same from within AppSheet (not clear if this is better):</p>
<ul>
<li>Navigate to the page listing apps (AppSheet&#8217;s main page), click &#8220;+ Create&#8221;, pick &#8220;App&#8221; and then &#8220;Start with existing data&#8221;.</li>
<li>Give the app a name. For category, I choose &#8220;Inspection &amp; Surveys&#8221;, and then click &#8220;Choose your data&#8221;.</li>
<li>Select an existing Google Sheets entry in your Google Drive account as the database (Google Sheets).</li>
<li>Trying to use an .ods file as the data source caused AppSheet to hang (wait indefinitely to complete creating the app).</li>
</ul>
<p>It&#8217;s also possible to use an AppSheet database for this purpose, but that seems to be a leftover from the days before AppSheet became part of Google. There&#8217;s no reason I&#8217;m aware of to prefer AppSheet database, and there&#8217;s no obvious way to make a backup of it, nor to restore. Possible with exporting to CSV with Automation, I suppose, but never tried it.</p>
<p>A directory named &#8220;appsheet&#8221; is created in Google Drives&#8217; home directory. It contains a directory structure with an empty file called &#8220;empty.txt&#8221;. A subdirectory is created for each AppSheet app, and files generated by the App go into that subdirectory. It&#8217;s possible to select a different target for output files, however (see below).</p>
<p>When an app is created, you get an email with an invitation to use it or develop it.</p>
<h3>To do after a while</h3>
<p>After playing around with an app for a while, it&#8217;s a good idea to make a few fixes. Go to the Views section (click on the third icon from the top) and then on the settings icon (a flywheel).</p>
<ul>
<li>In Views &gt; General, choose the starting View.</li>
<li>Also in Views &gt; General, set the <a rel="noopener" href="https://support.google.com/appsheet/answer/10106529?hl=en" target="_blank">Image Upload size</a> to the desired level (High for 1600x900 with my phone, of Full).</li>
<li>Be sure to have enabled &#8220;Allow drawing on images&#8221; on all Image columns (unless you don&#8217;t like that option).</li>
<li>In Offline mode, enable &#8220;Store content for offline use&#8221;.</li>
<li>Go do the Data view, choose the main table, and opt out deleting rows (the flywheel icon to the top right for a table&#8217;s settings). Delete rows directly on the spreadsheet if needed.</li>
<li>Change the Default app folder to /theprojectname/appsheet-data or something like that, which should be a valid path in Google Drive. So that the files land somewhere sensible. This is done in the Setting section (flywheel icon to the left), under &#8220;Information&#8221;.</li>
</ul>
<h3>Views</h3>
<ul>
<li>The View displays (or allows editing) a subset of column values of a row of a table. In some cases, several rows are listed in one View. Which columns are displayed, how they are displayed, and if they are editable, is what the View&#8217;s configuration is about. But no matter how you twist and turn it, a View shows values of columns (possibly along with the column names). If you want to display anything, turn it into a column (possibly a Virtual column, defined only inside AppSheet, see below).</li>
<li>The relation between the AppSheet&#8217;s table, which is used in Views and expressions, is not 1:1 with the table in the spreadsheet: Virtual columns are added, either by AppSheet itself (e.g. _RowNumber and Row ID), or by the user. In the latter case, the value of the Virtual Column is an expression that the user supplies. This can be a constant, for example if the purpose is to create a menu button in a View. The expression can also be a formula, which depends on the value of other columns of the same or other tables. In these expressions, [X] is the value of column X of the same row.</li>
<li>Clicking / tapping on an displayed item on a View initiates an <a rel="noopener" href="https://support.google.com/appsheet/answer/10107706" target="_blank">Action</a>, which often consists of opening a view for a specific row in a table for read-only access or editing. These are system-generated view with names like &#8220;main_detail&#8221;, &#8220;main_form&#8221; and &#8220;main_inline&#8221; for a table named &#8220;main&#8221;. These Views can be modified like any other view. And a whole lot of other actions can be added and used as the response.</li>
<li>It&#8217;s often difficult to figure out which View appears in response to clicks, as it&#8217;s often a system-generated one. Be sure to have the Edit option on (top-right on the development screen). Hover over the relevant area, wait for the pencil icon to appear, click it and pick e.g. &#8220;Edit View&#8221;.</li>
<li>If a system-generated view is deleted, it&#8217;s created anew after saving, apparently with default settings.</li>
<li>If a column is added to the spreadsheet&#8217;s table, it&#8217;s not available for use in the AppSheet immediately. The AppSheet&#8217;s table schema needs to be regenerated for this to happen (click the Data icon, click the table, three dots, and then &#8220;Regenerate Schema&#8221;). Virtual columns are not deleted, despite the scary warning one gets.</li>
<li>When clicking / tapping an item can result in an detailed view or the possibility to edit, these are system-generated views, that appear at the bottom left in the Views subsection. These Views can be modified.</li>
<li>Views listed under &#8220;Primary Navigation&#8221; are accessible directly at the bottom part of the screen. Those under &#8220;Menu Navigation&#8221; are accessible through the menu. &#8220;Reference Views&#8221; are invisible to the user, except for when requested from other views, for example in an Action (i.e. a response to clicking / tapping an item).</li>
<li>There are Format Rules allowing to change colors of items etc depending on boolean expressions. In most cases, they apply to columns and how they are displayed (with a different color, or with a colored icon added). Unfortunately, it&#8217;s impossible (as far as I know) to write catch-all rules for several columns, as the expression for activating the rule doesn&#8217;t play ball with the [_THIS] expression, which means &#8220;this column&#8221;. So for example, if you want a rule that marks unfilled columns with a red dot, add (or duplicate) one rule for each and every column, and be sure that the rule for column X doesn&#8217;t by mistake change the display format of column Y. It&#8217;s just an invitation to make silly bugs.</li>
<li>The column&#8217;s name is displayed above the column&#8217;s value in forms and detail Views by default. This can be changed by opening the column&#8217;s settings in the Data section, and change the &#8220;Display name&#8221;. So use concise names in the spreadsheet.</li>
<li>A column of type Show is useful for text, URLs, images etc, that appear in forms for instructions, setting page breaks etc. These are best added as virtual columns with literal values in the expression. Don&#8217;t forget to turn off the &#8220;Editable&#8221; attribute of this column. It&#8217;s quite unfortunate that instruction text appears as an extra column along with the spreadsheet&#8217;s data, but that&#8217;s the way it&#8217;s done.</li>
<li>To navigate to another view as a result of clicking on a column (possibly a Virtual Column acting as a menu entry):
<ul>
<li>First, an Action needs to be created. Click on the Action icon (looks like electric zap), click on the &#8220;+&#8221; at the top left, and add an action</li>
<li>Pick the table related to the view that this action shall work on, and pick &#8220;App: go to another view within this app&#8221;.</li>
<li>As for the Target, choose the expression LINKTOVIEW([targetview]) (given that &#8220;targetview&#8221; is the name of the column containing the name of the view to navigate to, as a plain text column).</li>
<li>Go to the relevant item in the View, and choose the newly created action under the Behavior section.</li>
<li>If the item is a Virtual Column just for the purpose of being a menu item, change its Display Name to &#8221; &#8221; (a normal white space inside quotes) so that the column&#8217;s name doesn&#8217;t appear on the menu, which is both ugly and consumes space. Choosing an empty string, &#8220;&#8221;, has no effect.</li>
</ul>
</li>
</ul>
<p>There are several types of Views, but the most important for a plain spreadsheet-based app are:</p>
<ul>
<li>Form: All columns listed in the &#8220;Column order&#8221; are shown and editable. The title of each input (text box, drop-down menu etc.) is the name of the column in the database or spreadsheet, unless overridden in the column&#8217;s configuration (in the Data section). Unfortunately, a back-swipe on a mobile phone is interpreted as &#8220;Cancel&#8221;, and there&#8217;s no way around this. One must explicitly tap on Save, or else all is info is lost.</li>
<li>Details: Like Form, but read-only, with an icon at the bottom right for editing the content, which switches to a Form with the same set of fields. Unfortunately, blank fields are not shown unless their Show property is set to the expression TRUE (just checking the checkbox isn&#8217;t good enough, it has to be done with an expression).</li>
<li>Table: As the name implies, like Details, but with the data shown as a table (with rows being rows, columns being columns). Selecting a row brings to a Details view of it. It&#8217;s possible to configure which columns appear in the table, possibly only one. So this can be a concise way to display a list of rows. It appears like the column marked as Label won&#8217;t appear in a Table View even if chosen to do so. Why is unclear.</li>
<li>Card and Deck: Each row gets a small pane with two or a few selected column values shown. Selecting one brings to a Details view.</li>
<li>Gallery: Shows the image associated with each row (if such exists) and the value of the column marked as Label.</li>
<li>Dashboard: A View containing other Views.</li>
</ul>
<h3>Adding a new table</h3>
<ul>
<li>In Appsheet, click the Data icon (the second icon from the top).</li>
<li>Click on the &#8220;+&#8221; (Add new data)</li>
<li>Navigate to the relevant Google Sheet.</li>
<li>Navigate to the related table, and add it.</li>
</ul>
<h3>Settings for each column in a table</h3>
<p>In the Data section, there are several attributes one should look at in a table&#8217;s configuration for a table:</p>
<ul>
<li>Name: Must match the name in the spreadsheet&#8217;s header (or the column name of a database&#8217;s table). The app fails to load otherwise.</li>
<li>Type: Number, text, image, reference, price, there are a lot of options. The automatic choice is usually correct for the simple textual columns. For more sophisticated input (drop-down menus, images, references etc.) this is the place to configure that.</li>
<li>Key: This checkbox is checked for one column only, selecting the key column in the database sense. Relevant in particular when using references.</li>
<li>Label: This checkbox is checked for one column only (plus, possibly, an image), selecting which column appears in several views as the row&#8217;s main description.</li>
<li>Formula: Left blank for columns taken from databases, must contain something for a Virtual Column. When set to something, the value of the column is the expression in this formula.</li>
<li>SHOW?: As its name implies, it controls if the column should be shown in views. This checkbox should usually be checked: If a column isn&#8217;t desired in a view, it should be removed from there. If the column should be displayed in Details Views even when blank, checking this isn&#8217;t enough: The Show propery must be set to TRUE as an expression.</li>
<li>EDITABLE?: Can the value of the column change?</li>
<li>REQUIRE?: Must the column have a value to allow finishing an edit session containing this column?</li>
<li>Initial value: The column&#8217;s initial value when a new row is created.</li>
<li>Display name: If left blank, the Name field from above appears above the value of this column in forms etc. Display name overrides this otherwise.</li>
<li>Description: For internal documentation, and is also used instead of the column name if &#8220;Display Name&#8221; is left blank.</li>
<li>Search: Is this column involved in searches? This is a tricky one: Enabling this means that the row will appear in searches even if the column isn&#8217;t displayed in the View, and vice versa: Even if a column is displayed, the search doesn&#8217;t take it into consideration unless the Search option is enabled for the column. It would, of course, had made more sense to define this option per View, but that&#8217;s not the way it works, unfortunately.</li>
<li>Scan, NFC, PII: Can this column accept data from these input methods?(scan means barcode scanning)</li>
</ul>
<h3>Modifying the spreadsheet</h3>
<p>It&#8217;s possible to modify the spreadsheet even after the app has been created, including adding a column in the middle. The immediate effect is an error (&#8220;unable to fetch app definition&#8221; on the mobile app, or just an error on the development console). To resolve this, pick the &#8220;Data&#8221; icon (second from the top&#8221;) on the development console, and then click the round arrow (as in &#8220;reload&#8221;) to regenerate the structure. A scary warning pops up in response to that, but just go ahead.</p>
<p>Note that the connection between AppSheet and the spreadsheet is through the names of the columns as given in the first row, as well as the name of the sheet. These should not be modified (unless the manipulation is intentional).</p>
<h3>References etc.</h3>
<p>AppSheet <a rel="noopener" href="https://support.google.com/appsheet/answer/10106510?hl=en&amp;ref_topic=10101919" target="_blank">definitely supports relational databases</a>. In fact, by choosing names of columns where one is the plural form of the other (&#8220;owner&#8221; column and &#8220;owners&#8221; table) the relation is possibly set up automatically. This is however not necessarily a good idea, because the tools choose which column&#8217;s value identifies the entire row &#8212; and getting this wrong could mess up things later, and it&#8217;s difficult to fix it afterwards.</p>
<p>Rather, if a spreadsheet (or virtual) column (in &#8220;table A&#8221;) is assigned the type &#8220;Ref&#8221;, it&#8217;s also required to assign the table referred to (&#8220;table B&#8221;). The value of the column selects the row that has a key column equal to it. In the View showing this column, the label column of the referred table is shown. Clicking / tapping on the label leads to a View of that row: its columns&#8217; values as well as other rows referencing it. In order to access a specific column, add a virtual column to Table A with a formula like [the_refcol_in_table_A].[the_col_in_table_B]. It&#8217;s not as difficult as is sounds, as the GUI starts suggesting completions when it sees the brackets and the dot.</p>
<p>Note that in Appsheet, a reference actually means the whole row. In many practical situations, only the label column is displayed, but it&#8217;s important to remember that conceptually, a Ref column represents the entire row it refers to.</p>
<p>And this brings me to back-reference. When a table B is referenced by a column somewhere in table A, a virtual column is automatically added to table B. This column has the type List and its value is something like REF_ROWS(&#8220;table A&#8221;, &#8220;referring_column&#8221;). The arguments are the name of table A and the name of the column with type Ref in that table. So this column lists all rows in table A that refer to the row in table B. Consider deselecting its &#8220;SHOW&#8221; checkbox in the Data view, if this back and forth is undesired.</p>
<p>Also, note that the back-reference is shown as a list of inline elements on the View of Table B. In order to determine how it appears, configure the system-generated view with name e.g. tableA_Inline (for example, if it&#8217;s a table or a deck). Even more important, it allows choosing what happens when one of the elements is clicked: View details (e.g. tableA_Detail view), edit (e.g. tableA_Form) or do something completely different with an Action?</p>
<p>And when references exist, it&#8217;s important to get the keys right. It&#8217;s possible to choose anything that is guaranteed to be unique, but because references relate to the key, there might be weird side-effects if the value of the key column changes. For example, if we choose a person&#8217;s email address as the key, all references to that row are lost if this person changes this address.</p>
<p>What we really want is a solid reference to the row itself. This is best done by adding a dedicated column for that in the table (i.e. in the spreadsheet), and assign it with a unique value when a new row is created: Use UNIQUEID() as this column&#8217;s initial value. When AppSheet adds a new row, it will calculate a unique ID and put the value there. Make this column non-editable (possibly also turn off &#8220;SHOW&#8221; too). In fact, AppSheet does this automatically when I give a column the name &#8220;key_id&#8221;.</p>
<p>Note that calculating UNIQUEID() in a virtual column is pointless. That&#8217;s not a solution for creating a unique ID for a row.</p>
<p>Other insights:</p>
<ul>
<li>Allowing upload of multiple images (or adding other items): Create a table consisting of &#8220;key_id&#8221; (used as key), &#8220;ref&#8221;, &#8220;image&#8221; and &#8220;weight&#8221; columns. The &#8220;ref&#8221; column is declared as a Ref type to the table in which the images will be stored. Enable &#8220;is part of&#8221;, which makes it possible to add new items from the main view. So all in all, there is a list of images in the main view, (of table B). Each row in the view points to a row in table A. This makes it possible to add an arbitrary number of items from a view of a row in the main view. Each item becomes a new row in table A, with a reference to table B. &#8220;weight&#8221; is a number, allowing to control the order when displaying the image. Using &#8220;key_id&#8221; as the key, and not &#8220;image&#8221; (which is the name to the file containing the image) makes it possible to show the same image in different Views, even from different tables.</li>
<li>Drop-down menus with text taken from somewhere else: Prepare a table (a sheet, and import it as a table) with keys as numbers and text (or anything else) as values. Then, on the table where we want the drop-down, set the column&#8217;s type to Ref, and point to the said tables as the reference. Select the input type to Buttons or Drop-down. In order to allow adding new possible values, set it to Enum (or EnumList for multiple values), and the Base type to Ref. Choose the said table as the target for the reference. Note however that in the latter method, all already existing keys must be added manually to the column&#8217;s definitions, exactly like any Enum type. The only difference is that the keys (possibly numbers) are fed manually, not the text. So if adding new possibilities is required, plain Enum is probably the best way.</li>
<li>It&#8217;s impossible to get a list of options not chosen in an EnumList. It&#8217;s possible to write an expression that reduces the chosen elements from a given list with those selected by an EnumList, but that requires keeping this expression and the EnumList&#8217;s value in sync. And if one forgets to update the expression after adding it to the EnumList, it&#8217;s a nasty bug.</li>
</ul>
<h3>One long row or relational database?</h3>
<p>As there are a lot of pieces of information about an apartment, which can be divided into categories and will be handled by different Views, my database manager self was inclined towards a relational database approach. One row with a few columns for each apartment in the &#8220;main table&#8221;, and then several tables, one for each information category (i.e. View), referenced by the &#8220;main table&#8221;.</p>
<p>The alternative approach is one table for all information (except for images and other utilities) and to make one long row with all info. It&#8217;s up to the Views to select which columns to show: All columns in the original table (or spreadsheet sheet) don&#8217;t have to be visible. It&#8217;s possible to make several views of the same table. But this means a lot of columns to handle (add virtual columns to that), making it difficult to keep track of the whole thing.</p>
<p>I went for the one long row approach. The main reason was that this approach makes it easier to reorganize the Views if needed. For example, if fields are added over time, and a View becomes too crowded, it&#8217;s not a big deal to split it into two Views, if they both refer to the same table anyhow. Or to move a field to another View.</p>
<p>That said, the separate table table approach would definitely have worked: When a new item is added, that means a row in the main table. A new row is created on each separate table by virtue of a menu button (a virtual column with a specified Action). LINKTOFORM() allows opening a form with specific initial values, so the key column can be set in the child table to the parent table&#8217;s key column value, ensuring a 1:1 relationship (and prevents duplicate rows).</p>
<p>For reports, the child table is shown as a back-reference to the main tables with a Start/End pair of template tags (maybe there&#8217;s a simpler way to do this).</p>
<p>In hindsight, I should have taken the separate table approach, but in a way I didn&#8217;t think about at first: There should have been a table with one row for each room, regardless of its type. This would have reduced the number of columns in the main table, and the amount of work and headache is linear to the number of columns. The trick would have been not to show all columns of a room&#8217;s table in the form View, but only those suitable for the specific type of room. The rules for which types of rooms exist, and which columns should be shown in the View would be listed in a separate table. Hence the &#8220;Show if&#8221; rule would look up the column in this rule table, in relation to the room&#8217;s type.</p>
<h3>Use slices?</h3>
<p>The question is how to display a subset of the columns in different Views.</p>
<p>The seemingly natural (and less preferred) way is by using <a rel="noopener" href="https://support.google.com/appsheet/answer/10106592?hl=en&amp;ref_topic=10101728" target="_blank">slices</a>, which is a virtual table, calculated on the fly, consisting of a subset of rows and columns as configured. This allows displaying only a subset of rows, columns and actions, filtered with a condition on the data (or just not show all columns). Slices are configured with the &#8220;+&#8221; icon on each data set (inside the &#8220;Data&#8221; section) as they are considered some form of table. Alternatively, slices are hinted on the View&#8217;s configuration as well.</p>
<p>This would have been the preferred way, had it not been for an annoying caveat: If a new column is added to the database (i.e. sheet), it appears in all slices. This makes slices a no-go for the purpose of selecting columns for a view.</p>
<p>Instead, the columns to display should be selected in the &#8220;Column order&#8221; part, which can be found in the &#8220;View Options&#8221; section for each View configuration. Note that &#8220;Column order&#8221; isn&#8217;t present for Views that don&#8217;t display columns, e.g. Deck and Gallery.</p>
<p>It makes sense to start from generating an empty view, and copying it for each time a new view is created.</p>
<h3>Creating a pdf file from the data of a row</h3>
<p>&#8230;or for that matter, a CSV / JSON / XML / HTML / XLSX file.</p>
<p>The important point is that this is done with Automation, and that the execution is triggered by an Event, and not as an Action. In other words, clicking something on a View won&#8217;t trigger the generation of the report directly. Rather, clicking the entry in the View causes an Action that modifies a row in a table, or to add a row a table, and that, in turn, triggers the Event.</p>
<p>There is no direct connection between the Action and the task it requests &#8212; the generation of a file, in this case. It can take 20-30 seconds from the triggering event until the file is created.</p>
<p>One option to request the generation of a file from an app: First, add two columns to the table for which the report is desired: &#8220;do_report&#8221; and &#8220;last_report&#8221;. In AppSheet, regenerate the schema if necessary, and set the columns as follows:</p>
<ul>
<li>do_report: Type is &#8220;Yes/No&#8221; (sweet talk for Boolean), turn off &#8220;show&#8221;, initial value &#8220;FALSE&#8221;</li>
<li>last_report: Type is &#8220;Text&#8221;, and set Initial Value to &#8220;Never&#8221;. &#8220;Editable&#8221; must remain <strong>enabled</strong>, even though it shouldn&#8217;t be accessible by forms: Its intention is to be set by the Bot, and this is possible only if the column is editable.</li>
<li>Add a virtual column named &#8220;generate_report&#8221;, &#8220;Text&#8221; type, with expression as follows:
<pre>CONCATENATE("Generate report
Last report: ", [last_report])</pre>
<p>Note that there is a new line in the middle of the string.</li>
<li>Create a new Action (Bzzt icon): For &#8220;Do this&#8221; pick &#8220;Data: set the values of some columns in this row&#8221;. Pick the &#8220;do_report&#8221; column and set the expression to TRUE. I&#8217;m going to use the Virtual Column defined above as a menu item, so for Position, pick &#8220;Inline&#8221; and at &#8220;Attach to column&#8221;, pick &#8220;generate_report&#8221;. In Behavior, set &#8220;Only if this condition is true&#8221; NOT([do_report]), and optionally also enable the request for confirmation.</li>
<li>Add the &#8220;generate_report&#8221; column to the View (or Views) from which the report should be requested.</li>
<li>Optionally (and recommended), add a Format rule for generate_report. The condition is [do_report] and if so, the text should be grey (color #888888) and in italics. For &#8220;Format these columns and actions&#8221; pick generate_report, and possibly also the action created just above. Without this, there is no immediate visual feedback to &#8220;pushing the button&#8221;.</li>
</ul>
<p>Now to the part that actually generates the report.</p>
<ul>
<li>Go to the Automation section (robot icon) and create a new Bot.</li>
<li>Click &#8220;Configure Event&#8221; and give it a name.</li>
<li>In the pane to the right, the Event source should be &#8220;App&#8221;. Pick the table from which the Event is triggered.</li>
<li>Select only &#8220;Updates&#8221; for Data change type.</li>
<li>Condition: [do_report]</li>
<li>Next, define what should be executed &#8212; the Process. Add Steps, which is more or less like a line in a computer program: Each of them can perform something, it can check a condition and branch, or wait for the condition to be true. Or call a process. These are steps to define (give each some name, as usual) and configure them on the pane to the right.
<ul>
<li>Type: &#8220;Run a data action&#8221;. In the right pane, pick &#8220;Set row values&#8221;, and set &#8220;do_report&#8221; to FALSE.</li>
<li>Type: &#8220;Run a task&#8221;. Set up the task as follows in the right pane:
<ul>
<li>Pick Create a new file</li>
<li>HTTP Content Type: pdf</li>
<li>Template: Click Create (or pick an existing template, if you have one from previous sessions). A &#8220;View&#8221; button appears after this is completed. More on templates in a separate section below.</li>
<li><em>Optional</em>: Change File Folder Path to &#8220;/&#8221; (with the quotes), File Name Prefix to &#8220;report&#8221; (once again, with the quotes) and opt-in Disable Timestamp. This way, report.pdf is created at the project&#8217;s root directory (<strong>not</strong> Google Drive&#8217;s root directory in most cases). Expressions can be used for these, of course.</li>
</ul>
</li>
<li>Type: &#8220;Run a data action&#8221;. In the right pane, pick &#8220;Set row values&#8221;, and set last_report to TEXT(NOW(), &#8220;DD/MM/YYYY HH:MM:SS&#8221;) for British time format.</li>
</ul>
</li>
</ul>
<p>Note that it&#8217;s possible to create a pdf file, but the template file is an HTML file. This makes it much easier to handle the template, and it&#8217;s always possible to go back to the HTML file to see what went wrong. The disadvantage with an HTML template is that the file generation fails on the tiniest HTML syntax error. For example, a missing &lt;/tr&gt; tag. Or even more ridiculous, a missing &lt;/p&gt; tag.</p>
<p>In order to view the generated file by clicking a menu item in a View, create a Virtual Row for this purpose, and assign it with an Action when selected. This action should be &#8220;External: Open a file&#8221;. The &#8220;File&#8221; entry is a URL to where the file is stored on Google drive. Obtain this link with the Share feature, so the expression becomes something like &#8220;https://drive.google.com/file/d/1d8AWHjLJdK9cyhYGBPnuVJJ0asmLmZVw/view?usp=sharing&#8221; (the quotes included). This is not a security risk as one chooses to share only with oneself (when obtaining the link from Google Drive).</p>
<p>If the execution fails due to a malformed template file (something inside the markups didn&#8217;t work fine), the app&#8217;s View doesn&#8217;t always get updated until a restart of the app (or reload the entire page on a web browser).</p>
<p>Actually, the way the whole thing behaves is that the menu button gets greyed out immediately after being pressed, and then gets back to normal after the file has been generated. But the color is determined by do_report&#8217;s status, which is turned back to FALSE before the attempt to generate a file. So if the file generation takes time, it may return to normal before the file is updated &#8212; but this has never had any practical consequence for me. But why doesn&#8217;t it go back to normal when the file generation fails? do_report is FALSE either way.</p>
<p><strong>To view error messages</strong>: Go to the &#8220;Monitor&#8221; section (icon at the left side, one from the bottom), and pick &#8220;Launch Log Analyzer&#8221; in the &#8220;Audit History&#8221; part. After some graphs, there&#8217;s a table with the last tasks initiated. Where there is an error, click the binocular icon, for details. A JSON text entry appears. The &#8220;Errors&#8221; key is where the error message is shown. It can take a few minutes before the entry with the error is shown, and before than, there may be entries showing success even if an error has occurred. So wait a few minutes before ruling out a failed run.</p>
<h3>Templates</h3>
<p>A template is a file in Doc or HTML format. Everything is passed through to the output document, except for markups with the &lt;&lt;expression&gt;&gt; form (or, for an HTML template, &amp;lt;&amp;lt;expression&amp;gt;&amp;gt;. The expression is evaluated exactly in the same way as the value of a Virtual Column, and the markup is substituted with the expression&#8217;s result. Hence to obtain a substitution with the value of a column named &#8220;thecol&#8221;, just insert &lt;&lt;[thecol]&gt;&gt;.</p>
<p>No matter if the output is pdf or HTML, when using HTML templates, be sure that the HTML file has all the classic wrapper tags: &lt;html&gt;, &lt;head&gt;, &lt;body&gt; etc. or else AppSheet ignores all &lt;&lt;something&gt;&gt; (or actually, &amp;lt;&amp;lt;something&amp;gt;&amp;gt;) markups altogether and the output is the template file itself.</p>
<p>There are special kinds of markups:</p>
<ul>
<li><a rel="noopener" href="https://support.google.com/appsheet/answer/11541779" target="_blank">Start/End</a> for looping on a list of keys to a table</li>
<li><a rel="noopener" href="https://support.google.com/appsheet/answer/11568425?hl=en&amp;ref_topic=11445504&amp;sjid=5954924214521688546-EU" target="_blank">If/EndIf markups</a>, allowing a segment to be included or not. Apparently not available with HTML templates, as it&#8217;s not listed explicitly, and I failed to use it (so I used the <a rel="noopener" href="https://support.google.com/appsheet/answer/10108198" target="_blank">IF() function</a> instead).</li>
</ul>
<p>The automatically created template is simple and not necessarily very helpful: This template encompasses all real columns (but not the virtual ones), showing the column&#8217;s name (not its display name) and their plain values. Its most valuable part is where it shows the use of Start/End (when relevant, i.e. when there are back-references to the displayed table). Note that as shown in the template, the Start and End tags must be inside a &lt;p&gt; block. They can&#8217;t be just a floating piece of code, or within another couple of tags (&lt;div&gt; doesn&#8217;t work, for example). Otherwise one gets a misleading error message saying the table with name &#8220;Start:&#8221; can&#8217;t be found + a lot of error messages for everything between the two tags.</p>
<p>The template is annoying in particular in relation to Ref types: The actual value, which is the key itself, is shown for references, and not the referenced value, as seen on the app&#8217;s Views. It would have been sweet of AppSheet to look up which of the referenced table&#8217;s columns is displayed in the View, and show that. But there&#8217;s another reason for this: Say, that the Ref column is &#8220;choice&#8221; and the column to display on the referenced table is &#8220;label&#8221;. The expression for the value to display is [choice].[label]. But if &#8220;choice&#8221; happens to be blank, and this expression appears anywhere in the template, the file generation fails. So be sure to assign an initial value to all Ref columns when creating a new row. Plus, possibly make a simple sanity check on the column, just in case:</p>
<pre>&lt;&lt;IF(ISBLANK([choice]), "Error!", [choice].[label])&gt;&gt;</pre>
<p>Not sure if this is bullet-proof, but it solves the most obvious problem.</p>
<p>I&#8217;ve also got the impression that if the result of the expression of an AppSheet markup expression returns anything containing HTML markups (e.g. &lt;b&gt; and &lt;/b&gt;) the file generation fails completely.</p>
<h3>Notes to self</h3>
<p><span class="yadayada">These are cryptic notes to myself, for my own apps&#8217; housekeeping tasks.</span></p>
<p><span class="yadayada">First of all, the account I&#8217;ve shared stuff with is &#8220;obscura&#8221;.</span></p>
<h4><span class="yadayada">Adding/removing a new item to a checklist</span></h4>
<ul>
<li><span class="yadayada">Update the related checkcnt expression with the new number of items, so that the report says all is done when it really is.</span></li>
</ul>
<h4><span class="yadayada">Adding a new column</span></h4>
<ul>
<li><span class="yadayada">Be sure to regenerate the schema of the related table in AppSheet.</span></li>
<li><span style="text-decoration: line-through;"><span class="yadayada">Possibly add (or duplicate) an &#8220;Unfilled X&#8221; Format rule (e.g. ISBLANK([streetaddress]) ), if it matters that the field is empty. Mark with Cyan dot, and don&#8217;t forget to set the affected column correctly.</span></span></li>
<li><span style="text-decoration: line-through;"><span class="yadayada">Set the SHOW property of the column to the expression TRUE if it&#8217;s supposed to appear in the detailed view, even if it&#8217;s blank. Just clicking on the &#8220;SHOW&#8221; checkbox isn&#8217;t good enough.</span></span></li>
<li><span class="yadayada">Set the Display property to something friendly</span></li>
</ul>
<p><span class="yadayada">The striked-out bullets were relevant when I thought about using Detail view as the summary of all. I used a pdf report now, so they&#8217;re not relevant.</span></p>
<h4><span class="yadayada">Adding a new View Form</span></h4>
<ul>
<li><span class="yadayada">There should be a status_* column in relation to this in the spreadsheet. Its setting inside AppSheet: Ref to const_group_status with Buttons (not &#8220;part of&#8221;), enable Show, Editable, Require, Initial Value is &#8220;0&#8243; (with quotes), Display name is &#8220;Status&#8221;. The four checkboxes to the right are left unchecked.</span></li>
<li><span class="yadayada">In the main table, add a Virtual Column with the same name as the Form View it relates to, e.g. view_base for status_base (so it&#8217;s easier to match between them later). Its type is Text, and its value is the relevant menu item&#8217;s description.</span></li>
<li><span class="yadayada">Only now, create a new Form view. In Column order, pick Manual, select all, unselect those not required. Be sure to keep the relevant status_* column.</span></li>
<li><span class="yadayada">Go to the &#8220;Actions&#8221; section (Bzzzt icon) and add a new action (actually, duplicate a similar one). Name it e.g. &#8220;Jump to base form&#8221;. For a record of &#8220;main&#8221;, Do this is &#8220;App: go to another view within this app&#8221;. Set Target to LINKTOROW([key_id], &#8220;view_base&#8221;) where &#8220;view_base&#8221; is the name of the View. Set Position to Inline, and Attach to column as the related Virtual Column. Pick a suitable icon too. Unfortunately, there is no catch-all action for navigating.</span></li>
<li><span class="yadayada">Duplicate a couple of &#8220;Mark incomplete&#8221; format rules, and adapt them to the new View: Change the expression to match the correct status_* column, and also the &#8220;Format these columns&#8221; to the relevant &#8220;view_*&#8221; with the same name. And test it. There&#8217;s no way around a couple of rules for each View.</span></li>
</ul>
]]></content:encoded>
			<wfw:commentRss>https://billauer.se/blog/2026/01/jots-on-appsheets/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Un-ignore /usr/lib/systemd/ in .gitignore with git repo on root filesystem</title>
		<link>https://billauer.se/blog/2025/12/git-unignore-subdirectory/</link>
		<comments>https://billauer.se/blog/2025/12/git-unignore-subdirectory/#comments</comments>
		<pubDate>Tue, 23 Dec 2025 14:27:35 +0000</pubDate>
		<dc:creator>eli</dc:creator>
				<category><![CDATA[Linux]]></category>
		<category><![CDATA[Software]]></category>
		<category><![CDATA[systemd]]></category>

		<guid isPermaLink="false">https://billauer.se/blog/?p=7183</guid>
		<description><![CDATA[Actually, this is about un-ignoring any subdirectory that is grandchild to an ignored directory. Running Linux Mint 22.2 (based upon Ubuntu 24.04), and having a git repository on root filesystem to keep track of the computer&#8217;s configuration, the vast majority of directories are ignored. One of the is /lib, however /lib/systemd/ should not be ignored, [...]]]></description>
			<content:encoded><![CDATA[<p>Actually, this is about un-ignoring any subdirectory that is grandchild to an ignored directory.</p>
<p>Running Linux Mint 22.2 (based upon Ubuntu 24.04), and having a git repository on root filesystem to keep track of the computer&#8217;s configuration, the vast majority of directories are ignored. One of the is /lib, however /lib/systemd/ should not be ignored, as it contains crucial files for the system&#8217;s configuration.</p>
<p>On other distributions, the relevant part in .gitignore usually goes:</p>
<pre><span class="yadayada">[ ... ]</span>
bin/
boot/
dev/
home/
<span class="punch">lib/*
!lib/systemd/
</span>lib64/
lib32/
libx32/
lost+found/
media/
mnt/
opt/
proc/
root/
run/
sbin/
<span class="yadayada">[ ... ]</span></pre>
<p>So lib/ isn&#8217;t ignored as a directory, but all its content, including subdirectories is. That allows for un-ignoring lib/systemd/ on the following row. That&#8217;s why lib/ isn&#8217;t ignore-listed like the other ones.</p>
<p>But on Linux Mint 22.2, /lib is a symbolic link to /usr/lib. And since git treats a symbolic link just like a file, /lib/systemd/ is treated as /usr/lib/systemd. Ignoring /lib as a directory has no effect, and un-ignoring /lib/systemd has no effect, because to git, this directory doesn&#8217;t even exist.</p>
<p>So go</p>
<pre>$ <strong>man gitignore</strong></pre>
<p>and try to figure out what to do. It&#8217;s quite difficult actually, but it boils down to this:</p>
<pre>usr/*
!usr/lib/
usr/lib/*
!usr/lib/systemd/</pre>
<p>It&#8217;s a bit tangled, but the point is that /usr/lib is un-ignored, then all its files are ignored, and then /usr/lib/systemd is un-ignored.</p>
<p>The only good part about this solution is that it works.</p>
]]></content:encoded>
			<wfw:commentRss>https://billauer.se/blog/2025/12/git-unignore-subdirectory/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Footnote and whole-page layout with wkhtmltopdf</title>
		<link>https://billauer.se/blog/2025/11/wkhtmltopdf-footnote/</link>
		<comments>https://billauer.se/blog/2025/11/wkhtmltopdf-footnote/#comments</comments>
		<pubDate>Sat, 29 Nov 2025 11:58:40 +0000</pubDate>
		<dc:creator>eli</dc:creator>
				<category><![CDATA[Linux]]></category>
		<category><![CDATA[Software]]></category>

		<guid isPermaLink="false">https://billauer.se/blog/?p=7178</guid>
		<description><![CDATA[This HTML code makes wkhtmltopdf create a single page with a footnote. If the external &#60;div&#62; is duplicated, separate pages are generated. &#60;html&#62; &#60;head&#62; &#60;meta http-equiv="Content-Type" content="text/html; charset=utf-8" /&#62; &#60;/head&#62; &#60;body&#62; &#60;div style="height: 1350px; display: flex; flex-direction: column; break-inside: avoid; border:1px solid #668;"&#62; This is just a test. &#60;div style="margin-top: auto;"&#62; This is a footnote [...]]]></description>
			<content:encoded><![CDATA[<p>This HTML code makes wkhtmltopdf create a single page with a footnote. If the external &lt;div&gt; is duplicated, separate pages are generated.</p>
<pre><span class="hljs-tag">&lt;<span class="hljs-name">html</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">http-equiv</span>=<span class="hljs-string">"Content-Type"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"text/html; charset=utf-8"</span> /&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">style</span>=<span class="hljs-string">"height: 1350px; display: flex; flex-direction: column; break-inside: avoid; border:1px solid #668;"</span>&gt;</span>
This is just a test.
<span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">style</span>=<span class="hljs-string">"margin-top: auto;"</span>&gt;</span>
This is a footnote
<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span></pre>
<p>So how does it work? The important part is the &#8220;style&#8221; attribute of the outer &lt;div&gt; tag:</p>
<ul>
<li><strong>height: 1350px</strong>: This sets the &lt;div&gt; block&#8217;s height to a full A4 page. Why 1350 pixels? I don&#8217;t know. I just tweaked with this figure until it got right. It&#8217;s possible another figure is needed on a different version of wkhtmltopdf. I&#8217;ve tried to set this with cm as well as pt units, but none corresponded to the standard figures for an A4 page. So I went with pixels, which clarifies that it&#8217;s a wild guess.</li>
<li><strong>display: flex; flex-direction: column</strong>: This turns this &lt;div&gt; block into a Flexbox container, with vertical packing. This is needed to push the footnote&#8217;s block to the bottom.</li>
<li><strong>break-inside: avoid</strong>: This tells wkhtmltopdf to avoid page breaks in the middle of the block. This makes no difference for a single page, but if this &lt;div&gt; block is repeated, this style attribute ensures that each block gets a separate page (unless any of the pages exceeds a page&#8217;s height).</li>
<li><strong>border:1px solid #668</strong>: This generates a border around the &lt;div&gt; block&#8217;s occupied area. Used only for finding the correct height attribute, and should should be removed afterwards (unless this border is desired on every page).</li>
</ul>
<p>The footnote is pushed to the bottom of the page by virtue of the <strong>margin-top: auto</strong> style attribute and the fact that the &lt;div&gt; block having this attribute is within a vertical packed Flexbox container.</p>
<p><strong>Notes:</strong></p>
<ul>
<li>This was done with wkhtmltopdf 0.12.4, without the &#8220;wkhtmltopdf patches&#8221; according to the man page.</li>
<li>If the height is too large on any page, all break-inside are ignored. In other words, the whole pdf document gets garbled, not just around the page that went wrong.</li>
<li>I tried changing the resolution on my X11 display, and it didn&#8217;t make any difference. This might sound like a silly thing to check, but wkhtmltopdf depends on the X11 server.</li>
</ul>
]]></content:encoded>
			<wfw:commentRss>https://billauer.se/blog/2025/11/wkhtmltopdf-footnote/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Running KVM on Linux Mint 19 random jots</title>
		<link>https://billauer.se/blog/2024/07/virtualization-notes-to-self-2/</link>
		<comments>https://billauer.se/blog/2024/07/virtualization-notes-to-self-2/#comments</comments>
		<pubDate>Fri, 12 Jul 2024 14:54:45 +0000</pubDate>
		<dc:creator>eli</dc:creator>
				<category><![CDATA[Linux]]></category>
		<category><![CDATA[Software]]></category>
		<category><![CDATA[Virtualization]]></category>

		<guid isPermaLink="false">https://billauer.se/blog/?p=7094</guid>
		<description><![CDATA[General Exactly like my previous post from 14 years ago, these are random jots that I took as I set up a QEMU/KVM-based virtual machine on my Linux Mint 19 computer. This time, the purpose was to prepare myself for moving a server from an OpenVZ container to KVM. Other version details, for the record: [...]]]></description>
			<content:encoded><![CDATA[<h3>General</h3>
<p>Exactly like <a rel="noopener" href="https://billauer.se/blog/2010/01/virtualization-notes-to-self/" target="_blank">my previous post</a> from 14 years ago, these are random jots that I took as I set up a QEMU/KVM-based virtual machine on my Linux Mint 19 computer. This time, the purpose was to prepare myself for moving a server from an OpenVZ container to KVM.</p>
<p>Other version details, for the record: libvirt version 4.0.0, QEMU version 2.11.1, Virtual Machine manager 1.5.1.</p>
<h3>Installation</h3>
<p>Install some relevant packages:</p>
<pre># apt install qemu-kvm qemu-utils libvirt-daemon-system libvirt-clients virt-manager virt-viewer ebtables ovmf</pre>
<p>This clearly installed a few services: libvirt-bin, libvirtd, libvirt-guest, virtlogd, qemu-kvm, ebtables, and a couple of sockets: virtlockd.socket and virtlogd.socket with their attached services.</p>
<p>My regular username on the computer was added automatically to the &#8220;libvirt&#8221; group, however that doesn&#8217;t take effect until one logs out and and in again. Without belonging to this group, one gets the error message &#8220;Unable to connect to libvirt qemu:///system&#8221; when attempting to run the Virtual Machine Manager. Or in more detail: &#8220;libvirtError: Failed to connect socket to &#8216;/var/run/libvirt/libvirt-sock&#8217;: Permission denied&#8221;.</p>
<p>The lazy and temporary solution is to run the Virtual Machine Manager with &#8220;sg&#8221;. So instead of the usual command for starting the GUI tool (NOT as root):</p>
<pre>$ virt-manager &amp;</pre>
<p>Use &#8220;sg&#8221; (or start a session with the &#8220;newgroup&#8221; command):</p>
<pre>$ sg libvirt virt-manager &amp;</pre>
<p>This is necessary only until next time you log in to the console. I think. I didn&#8217;t get that far. Who logs out?</p>
<p>There&#8217;s also a command-line utility, virsh. For example, to list all running machines:</p>
<pre>$ sudo virsh list</pre>
<p>Or just &#8220;sudo virsh&#8221; for an interactive shell.</p>
<p>Note that without root permissions, the list is simply empty. This is really misleading.</p>
<h3>General notes</h3>
<ul>
<li>Virtual machines are called &#8220;domains&#8221; in several contexts (within virsh in particular).</li>
<li>To get the mouse out of the graphical window, use Ctrl-Alt.</li>
<li>For networking to work, some rules related to virbr0 are automatically added to the iptables firewall. If these are absent, go &#8220;systemctl restart libvirtd&#8221; (don&#8217;t do this with virtual machines running, of course).</li>
<li>These iptables rules are important in particular for WAN connections. Apparently, these allow virbr0 to make DNS queries to the local machine (adding rules to INPUT and OUTPUT chains). In addition, the FORWARD rule allows forwarding anything to and from virbr0 (as long as the correct address mask is matched). Plus a whole lot off stuff around POSTROUTING. Quite disgusting, actually.</li>
<li>There are two Ethernet interfaces related to KVM virtualization: vnet0 and virbr0 (typically). For sniffing, virbr0 is a better choice, as it&#8217;s the virtual machine&#8217;s own bridge to the system, so there is less noise. This is also the interface that has an IP address of its own.</li>
<li>A vnetN pops up for each virtual machine that is running, virbr0 is there regardless.</li>
<li>The configuration files are kept as fairly readable XML files in /etc/libvirt/qemu</li>
<li>The images are typically held at /var/lib/libvirt/images, owned by root with 0600 permissions.</li>
<li>The libvirtd service runs /usr/sbin/libvirtd as well as two processes of /usr/sbin/dnsmasq. When a virtual machine runs, it also runs an instance of qemu-system-x86_64 on its behalf.</li>
</ul>
<h3>Creating a new virtual machine</h3>
<p>Start the Virtual Manager. The GUI is good enough for my purposes.</p>
<pre>$ sg libvirt virt-manager &amp;</pre>
<ul>
<li>Click on the &#8220;Create new virtual machine&#8221; and choose &#8220;Local install media&#8221;. Set the other parameters as necessary.</li>
<li>As for storage, choose &#8220;Select or create custom storage&#8221; and create a qcow2 volume in a convenient position on the disk (/var/lib/libvirt/images is hardly a good place for that, as it&#8217;s on the root partition).</li>
<li>In the last step, choose &#8220;customize configuration before install&#8221;.</li>
<li>Network selection: Virtual nework &#8216;default&#8217;: NAT.</li>
<li>Change the NIC, Disk and Video to VirtIO as mentioned below.</li>
<li>Click &#8220;Begin Installation&#8221;.</li>
</ul>
<h3>Do it with VirtIO</h3>
<p>That is, use Linux&#8217; paravirtualization drivers, rather than emulation of hardware.</p>
<p>To set up a machine&#8217;s settings, go View &gt; Details.</p>
<p>This is lspci&#8217;s response with a default virtual machine:</p>
<pre>00:00.0 Host bridge: Intel Corporation 440FX - 82441FX PMC [Natoma] (rev 02)
00:01.0 ISA bridge: Intel Corporation 82371SB PIIX3 ISA [Natoma/Triton II]
00:01.1 IDE interface: Intel Corporation 82371SB PIIX3 IDE [Natoma/Triton II]
00:01.3 Bridge: Intel Corporation 82371AB/EB/MB PIIX4 ACPI (rev 03)
00:02.0 VGA compatible controller: Red Hat, Inc. QXL paravirtual graphic card (rev 04)
00:03.0 Ethernet controller: Realtek Semiconductor Co., Ltd. RTL-8100/8101L/8139 PCI Fast Ethernet Adapter (rev 20)
00:04.0 Audio device: Intel Corporation 82801FB/FBM/FR/FW/FRW (ICH6 Family) High Definition Audio Controller (rev 01)
00:05.0 USB controller: Intel Corporation 82801I (ICH9 Family) USB UHCI Controller #1 (rev 03)
00:05.1 USB controller: Intel Corporation 82801I (ICH9 Family) USB UHCI Controller #2 (rev 03)
00:05.2 USB controller: Intel Corporation 82801I (ICH9 Family) USB UHCI Controller #3 (rev 03)
00:05.7 USB controller: Intel Corporation 82801I (ICH9 Family) USB2 EHCI Controller #1 (rev 03)
00:06.0 Communication controller: Red Hat, Inc Virtio console
00:07.0 Unclassified device [00ff]: Red Hat, Inc Virtio memory balloon</pre>
<p>Cute, but all interfaces are emulations of real hardware. In other words, this will run really slowly.</p>
<p>Testing link speed: On the host machine:</p>
<pre>$ nc -l 1234 &lt; /dev/null &gt; /dev/null</pre>
<p>And on the guest:</p>
<pre>$ dd if=/dev/zero bs=128k count=4k | nc -q 0 10.1.1.3 1234
4096+0 records in
4096+0 records out
536870912 bytes (537 MB, 512 MiB) copied, 3.74558 s, 143 MB/s</pre>
<p>Quite impressive for hardware emulation, I must admit. But it can get better.</p>
<p>Things to change from the default settings:</p>
<ul>
<li>NIC: Choose &#8220;virtio&#8221; as device model, keep &#8220;Virtual network &#8216;default&#8217;&#8221; as NAT.</li>
<li>Disk: On &#8220;Disk bus&#8221;, don&#8217;t use IDE, but rather &#8220;VirtIO&#8221; (it will appear as /dev/vda etc.).</li>
<li>Video: Don&#8217;t use QXL, but Virtio (without 3D acceleration, it wasn&#8217;t supported on my machine). Actually, I&#8217;m not so sure about this one. For example, Ubuntu&#8217;s installation live boot gave me a black screen occasionally with Virtio.</li>
</ul>
<p>Note that it&#8217;s possible to use a VNC server instead of &#8220;Display spice&#8221;.</p>
<p>After making these changes:</p>
<pre>00:00.0 Host bridge: Intel Corporation 440FX - 82441FX PMC [Natoma] (rev 02)
00:01.0 ISA bridge: Intel Corporation 82371SB PIIX3 ISA [Natoma/Triton II]
00:01.1 IDE interface: Intel Corporation 82371SB PIIX3 IDE [Natoma/Triton II]
00:01.3 Bridge: Intel Corporation 82371AB/EB/MB PIIX4 ACPI (rev 03)
00:02.0 VGA compatible controller: Red Hat, Inc <span class="punch">Virtio</span> GPU (rev 01)
00:03.0 Ethernet controller: Red Hat, Inc <span class="punch">Virtio</span> network device
00:04.0 Audio device: Intel Corporation 82801FB/FBM/FR/FW/FRW (ICH6 Family) High Definition Audio Controller (rev 01)
00:05.0 USB controller: Intel Corporation 82801I (ICH9 Family) USB UHCI Controller #1 (rev 03)
00:05.1 USB controller: Intel Corporation 82801I (ICH9 Family) USB UHCI Controller #2 (rev 03)
00:05.2 USB controller: Intel Corporation 82801I (ICH9 Family) USB UHCI Controller #3 (rev 03)
00:05.7 USB controller: Intel Corporation 82801I (ICH9 Family) USB2 EHCI Controller #1 (rev 03)
00:06.0 Communication controller: Red Hat, Inc Virtio console
00:07.0 Unclassified device [00ff]: Red Hat, Inc Virtio memory balloon
00:08.0 SCSI storage controller: Red Hat, Inc <span class="punch">Virtio</span> block device</pre>
<p>Try the speed test again?</p>
<pre>$ dd if=/dev/zero bs=128k count=4k | nc -q 0 10.1.1.3 1234
4096+0 records in
4096+0 records out
536870912 bytes (537 MB, 512 MiB) copied, 0.426422 s, 1.3 GB/s</pre>
<p>Almost ten times faster.</p>
<h3>Preparing a live Ubuntu ISO for ssh</h3>
<pre>$ sudo su
# apt install openssh-server
# passwd ubuntu</pre>
<p>In the installation of the openssh-server, there&#8217;s a question of which configuration files to use. Choose the package maintainer&#8217;s version.</p>
]]></content:encoded>
			<wfw:commentRss>https://billauer.se/blog/2024/07/virtualization-notes-to-self-2/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Add \subsubsubsection to a Hitec Latex document</title>
		<link>https://billauer.se/blog/2024/07/latex-hitec-subsubsubsection/</link>
		<comments>https://billauer.se/blog/2024/07/latex-hitec-subsubsubsection/#comments</comments>
		<pubDate>Sat, 06 Jul 2024 04:39:08 +0000</pubDate>
		<dc:creator>eli</dc:creator>
				<category><![CDATA[Software]]></category>

		<guid isPermaLink="false">https://billauer.se/blog/?p=7084</guid>
		<description><![CDATA[So what if you need to divide a \subsubsection{} into even lower subsections? LaTeX classes don&#8217;t usually support that, because if you need that feature, your document&#8217;s structure is wrong. Or so they say. You should have chopped the document with \part{} or \chapter{} at a higher level, and not cut down the sections into [...]]]></description>
			<content:encoded><![CDATA[<p>So what if you need to divide a \subsubsection{} into even lower subsections? LaTeX classes don&#8217;t usually support that, because if you need that feature, your document&#8217;s structure is wrong. Or so they say. You should have chopped the document with \part{} or \chapter{} at a higher level, and not cut down the sections into even smaller pieces.</p>
<p>But with technical documentation (say, outlining an API) it can be very handy with something below \subsubsection{}. As it turns out, LaTeX actually supports lower levels, but they aren&#8217;t numbered by default. So it goes:</p>
<ol>
<li>\section{}</li>
<li>\subsection{}</li>
<li>\subsubsection{}</li>
<li>\paragraph{}</li>
<li>\subparagraph{}</li>
</ol>
<p>That&#8217;s neat, isn&#8217;t it? In order to make the two last numbered, add this to the LaTeX document:</p>
<pre><span class="hljs-keyword">\setcounter</span>{secnumdepth}{5}
<span class="hljs-keyword">\setcounter</span>{tocdepth}{5}
<span class="hljs-keyword">\titleformat</span>{<span class="hljs-keyword">\paragraph</span>}
{<span class="hljs-keyword">\normalfont</span><span class="hljs-keyword">\normalsize</span><span class="hljs-keyword">\bfseries</span>}{<span class="hljs-keyword">\theparagraph</span>}{1em}{}
<span class="hljs-keyword">\titlespacing</span>*{<span class="hljs-keyword">\paragraph</span>}
{-15ex}{3.25ex plus 1ex minus .2ex}{1.5ex plus .2ex}
<span class="hljs-keyword">\titleformat</span>{<span class="hljs-keyword">\subparagraph</span>}
{<span class="hljs-keyword">\normalfont</span><span class="hljs-keyword">\normalsize</span><span class="hljs-keyword">\bfseries</span>}{<span class="hljs-keyword">\thesubparagraph</span>}{1em}{}
<span class="hljs-keyword">\titlespacing</span>*{<span class="hljs-keyword">\subparagraph</span>}
{-12ex}{3.25ex plus 1ex minus .2ex}{1.5ex plus .2ex}</pre>
<p>After adding this, sub-sub-sub-section numbers appear with \paragraph{}, and even one more level down with \subparagraph{}.</p>
<p>\label{} works as expected (\ref{} correctly references \paragraph{} and \subparagraph{}), and the table of contents also lists these elements neatly.</p>
<p>This snippet works well with the <a rel="noopener" href="https://billauer.se/blog/2009/01/latex-hitec-class/" target="_blank">Hitec class</a>. I don&#8217;t know if it works with other classes. But even if it does, odds are that the result will look ugly, as this code defines the spacing so that it looks fairly nice with Hitec&#8217;s formatting.</p>
<p>So it&#8217;s not really \subsubsubsection{}, which is awkwardly long anyhow, but a more elegant solution.</p>
]]></content:encoded>
			<wfw:commentRss>https://billauer.se/blog/2024/07/latex-hitec-subsubsubsection/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>A little #define macro in C for selecting a bit range from an integer</title>
		<link>https://billauer.se/blog/2024/04/bit-range-c-macro/</link>
		<comments>https://billauer.se/blog/2024/04/bit-range-c-macro/#comments</comments>
		<pubDate>Sun, 21 Apr 2024 11:24:29 +0000</pubDate>
		<dc:creator>eli</dc:creator>
				<category><![CDATA[Software]]></category>

		<guid isPermaLink="false">https://billauer.se/blog/?p=7050</guid>
		<description><![CDATA[This is a simple utility C macro for selecting a bit range from an integer: #define bits(x, y, z) (((x) &#62;&#62; (z)) &#38; ((((long int) 2) &#60;&#60; ((y) - (z))) - 1)) This picks the part that is equivalent to the expression x[y:z] in Verilog. The cast to long int may need adjustment to the [...]]]></description>
			<content:encoded><![CDATA[<p>This is a simple utility C macro for selecting a bit range from an integer:</p>
<pre><span class="hljs-meta">#<span class="hljs-keyword">define</span> bits(x, y, z) (((x) &gt;&gt; (z)) &amp; ((((long int) 2) &lt;&lt; ((y) - (z))) - 1))</span></pre>
<p>This picks the part that is equivalent to the expression x[y:z] in Verilog.</p>
<p>The cast to long int may need adjustment to the type of the variable that is manipulated.</p>
<p>And yes, it&#8217;s possible this could have been done with less parentheses. But with macros, I&#8217;m always compelled to avoid any ambiguity that I may not thing about right now.</p>
]]></content:encoded>
			<wfw:commentRss>https://billauer.se/blog/2024/04/bit-range-c-macro/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Google Chrome: Stop that nagging on updates</title>
		<link>https://billauer.se/blog/2023/06/google-chrome-disable-update-popup/</link>
		<comments>https://billauer.se/blog/2023/06/google-chrome-disable-update-popup/#comments</comments>
		<pubDate>Sun, 11 Jun 2023 08:53:01 +0000</pubDate>
		<dc:creator>eli</dc:creator>
				<category><![CDATA[Internet]]></category>
		<category><![CDATA[Software]]></category>
		<category><![CDATA[stop updates]]></category>

		<guid isPermaLink="false">https://billauer.se/blog/?p=6892</guid>
		<description><![CDATA[I have Google Chrome installed on a Linux machine at /opt/google as root, so the browser can&#8217;t update itself automatically. Instead, it complains with this pop-up every time the browser is started: What I really like about this pop-up is the &#8220;you&#8217;re missing out&#8221; part. I get the same thing from the silly image gallery [...]]]></description>
			<content:encoded><![CDATA[<p>I have Google Chrome installed on a Linux machine at /opt/google as root, so the browser can&#8217;t update itself automatically. Instead, it complains with this pop-up every time the browser is started:</p>
<p><a href="https://billauer.se/blog/wp-content/uploads/2023/06/reinstall.jpg"><img class="aligncenter size-full wp-image-6893" title="Annoying Google Chrome update popup" src="https://billauer.se/blog/wp-content/uploads/2023/06/reinstall.jpg" alt="Annoying Google Chrome update popup" width="324" height="180" /></a></p>
<p>What I really like about this pop-up is the &#8220;you&#8217;re missing out&#8221; part. I get the same thing from the silly image gallery app on my Google Pixel phone.  This is Google trying to play on my (not so existent) FOMO.</p>
<p>It has been <a rel="noopener" href="https://stackoverflow.com/questions/27962454/disable-chrome-is-out-of-date-notification" target="_blank">suggested</a> to add the &#8211;simulate-outdated-no-au argument to the command line that executes Chrome. This works indeed. The common suggestion is however to do that on the shortcut that executes the browser. But that won&#8217;t cover the case when I run the browser from a shell. Something I do, every now and then. Don&#8217;t ask.</p>
<p>So a more sledge hammer solution is to edit the wrapper script:</p>
<pre>$ which google-chrome
/usr/bin/google-chrome</pre>
<p>So edit this file (as root), and change the last line from</p>
<pre>exec -a "$0" "$HERE/chrome" "$@"</pre>
<p>to</p>
<pre>exec -a "$0" "$HERE/chrome" <span class="punch">--simulate-outdated-no-au='Tue, 31 Dec 2099'</span> "$@"</pre>
<p>What does this mean, then? Well, according to the list of <a rel="noopener" href="https://chromium.googlesource.com/chromium/src/+/HEAD/chrome/common/chrome_switches.cc" target="_blank">Google Chrome switches</a>, this switch &#8220;simulates that current version is outdated and auto-update is off&#8221;. The date is referred to in the source&#8217;s <a rel="noopener" href="https://source.chromium.org/chromium/chromium/src/+/HEAD:chrome/browser/upgrade_detector/upgrade_detector_impl.cc" target="_blank">upgrade_detector_impl.cc</a>. Look there if you want to figure out why this works (I didn&#8217;t bother, actually).</p>
]]></content:encoded>
			<wfw:commentRss>https://billauer.se/blog/2023/06/google-chrome-disable-update-popup/feed/</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>Google Translate, LaTeX and asian languages: Technical notes</title>
		<link>https://billauer.se/blog/2022/08/google-translate-pdflatex-technical/</link>
		<comments>https://billauer.se/blog/2022/08/google-translate-pdflatex-technical/#comments</comments>
		<pubDate>Mon, 15 Aug 2022 07:18:50 +0000</pubDate>
		<dc:creator>eli</dc:creator>
				<category><![CDATA[Internet]]></category>
		<category><![CDATA[perl]]></category>
		<category><![CDATA[Software]]></category>

		<guid isPermaLink="false">https://billauer.se/blog/?p=6665</guid>
		<description><![CDATA[Introduction These post contains a few technical notes of using Google Translate for translating LaTeX documents into Chinese, Japanese and Korean. The insights on the language-related issues are written down in a separate post. Text vs. HTML Google&#8217;s cloud translator can be fed with either plain text or HTML, and it returns the same format. [...]]]></description>
			<content:encoded><![CDATA[<h3>Introduction</h3>
<p>These post contains a few technical notes of using Google Translate for translating LaTeX documents into Chinese, Japanese and Korean. The insights on the language-related issues are written down in a <a title="Translating technical documentation with Google Translate" href="https://billauer.se/blog/2022/08/google-translate-insights/" target="_blank">separate post</a>.</p>
<h3>Text vs. HTML</h3>
<p>Google&#8217;s cloud translator can be fed with either plain text or HTML, and it returns the same format. Plain text format is out of the question for anything but translating short sentences, as it becomes impossible to maintain the text&#8217;s formatting. So I went for the HTML interface.</p>
<p>The thing with HTML is that whitespaces can take different forms and shapes, and they are redundant in many situations. For example, a newline is often equivalent to a plain space, and neither make any difference between two paragraphs that are enclosed by &lt;p&gt; tags.</p>
<p>Google Translate takes this notion to the extreme, and typically removes all newlines from the original text. OK, that&#8217;s understandable. But it also adds and removes whitespaces where it had no business doing anything, in particular around meaningless segments that aren&#8217;t translated anyhow. This makes it quite challenging when feeding the results for further automatic processing.</p>
<h3>Setting up a Google Cloud account</h3>
<p>When creating a new Google Cloud account, there&#8217;s an automatic credit of $300 to spend for three months. So there&#8217;s plenty of room for much needed experimenting. Too see the status of the evaluation period, go to Billing &gt; Cost Breakdown and wait a minute or so for the &#8220;Free trial status&#8221; strip to appear at the top of the page. There&#8217;s no problem with &#8220;activating full account&#8221; immediately. The free trial credits remain, but it also means that real billing occurs when the credits are consumed and/or the trial period is over.</p>
<p>First create a new Google cloud account and enable the Google Translate API.</p>
<p>I went for Basic v2 translation (and not Advanced, v3). Their pricing is the same, but v3 is not allowed with an API key, and I really wasn&#8217;t into setting up a service account and struggle with OAuth2. The main advantage with v3 is the possibility to train the machine to adapt to a specific language pattern, but as mentioned in <a title="Translating technical documentation with Google Translate" href="https://billauer.se/blog/2022/08/google-translate-insights/" target="_blank">that separate post</a>, I&#8217;m hiding away anything but common English language patterns.</p>
<p>As for authentication, I went for <a rel="noopener" href="https://cloud.google.com/docs/authentication/api-keys" target="_blank">API keys</a>. I don&#8217;t need any personalized info, so that&#8217;s the simple way to go. To obtain the keys, go to main menu (hamburger icon) &gt; APIs and services &gt; Credentials and pick Create Credentials, and choose to create API keys. Copy the string and use it in the key=API_KEY parameters in POST requests. It&#8217;s possible to restrict the usage of this key in various ways (HTTP referrer, IP address etc.) but it wasn&#8217;t relevant in my case, because the script runs only on my computer.</p>
<p>The web interface for setting up cloud services is horribly slow, which is slightly ironic and a bit odd for a company like Google.</p>
<h3>The translation script</h3>
<p>I wrote a simple script for taking a piece of text in English and translating it into the language of choice:</p>
<pre><span class="hljs-comment">#!/usr/bin/perl</span>

<span class="hljs-keyword">use</span> warnings;
<span class="hljs-keyword">use</span> strict;
<span class="hljs-keyword">use</span> LWP::UserAgent;
<span class="hljs-keyword">use</span> JSON <span class="hljs-string">qw[ from_json ]</span>;

<span class="hljs-keyword">our</span> $WASTEMONEY = <span class="hljs-number">0</span>; <span class="hljs-comment"># Prompt before making request</span>
<span class="hljs-keyword">my</span> $MAXLEN = <span class="hljs-number">500000</span>;
<span class="hljs-keyword">my</span> $chars_per_dollar = <span class="hljs-number">50000</span>; <span class="hljs-comment"># $20 per million characters</span>

<span class="hljs-keyword">our</span> $APIkey = <span class="hljs-string">'your API key here'</span>;

<span class="hljs-keyword">my</span> ($outfile, $origfile, $lang) = @ARGV;

<span class="hljs-keyword">die</span>(<span class="hljs-string">"Usage: $0 outfile origfile langcode\n"</span>)
  <span class="hljs-keyword">unless</span> (<span class="hljs-keyword">defined</span> $origfile);

<span class="hljs-keyword">my</span> $input = readfile($origfile);

askuser() <span class="hljs-keyword">unless</span> ($WASTEMONEY);

<span class="hljs-keyword">my</span> $len = <span class="hljs-keyword">length</span> $input;

<span class="hljs-keyword">die</span>(<span class="hljs-string">"Cowardly refusing to translate $len characters\n"</span>)
  <span class="hljs-keyword">if</span> ($len &gt; $MAXLEN);

writefile($outfile, translate($input, $lang));

<span class="hljs-comment">################## SUBROUTINES ##################</span>

<span class="hljs-function"><span class="hljs-keyword">sub</span> <span class="hljs-title">writefile</span> </span>{
  <span class="hljs-keyword">my</span> ($fname, $data) = @_;

  <span class="hljs-keyword">open</span>(<span class="hljs-keyword">my</span> $out, <span class="hljs-string">"&gt;"</span>, $fname)
    <span class="hljs-keyword">or</span> <span class="hljs-keyword">die</span> <span class="hljs-string">"Can't open \"$fname\" for write: $!\n"</span>;
  <span class="hljs-keyword">binmode</span>($out, <span class="hljs-string">":utf8"</span>);
  <span class="hljs-keyword">print</span> $out $data;
  <span class="hljs-keyword">close</span> $out;
}

<span class="hljs-function"><span class="hljs-keyword">sub</span> <span class="hljs-title">readfile</span> </span>{
  <span class="hljs-keyword">my</span> ($fname) = @_;

  <span class="hljs-keyword">local</span> $/; <span class="hljs-comment"># Slurp mode</span>

  <span class="hljs-keyword">open</span>(<span class="hljs-keyword">my</span> $in, <span class="hljs-string">"&lt;"</span>, $fname)
    <span class="hljs-keyword">or</span> <span class="hljs-keyword">die</span> <span class="hljs-string">"Can't open $fname for read: $!\n"</span>;

  <span class="hljs-keyword">my</span> $input = &lt;$in&gt;;
  <span class="hljs-keyword">close</span> $in;

  <span class="hljs-keyword">return</span> $input;
}

<span class="hljs-function"><span class="hljs-keyword">sub</span> <span class="hljs-title">askuser</span> </span>{
  <span class="hljs-keyword">my</span> $len = <span class="hljs-keyword">length</span> $input;
  <span class="hljs-keyword">my</span> $cost = <span class="hljs-keyword">sprintf</span>(<span class="hljs-string">'$%.02f'</span>, $len / $chars_per_dollar);

  <span class="hljs-keyword">print</span> <span class="hljs-string">"\n\n*** Approval to access Google Translate ***\n"</span>;
  <span class="hljs-keyword">print</span> <span class="hljs-string">"$len bytes to $lang, $cost\n"</span>;
  <span class="hljs-keyword">print</span> <span class="hljs-string">"Source file: $origfile\n"</span>;
  <span class="hljs-keyword">print</span> <span class="hljs-string">"Proceed? [y/N] "</span>;

  <span class="hljs-keyword">my</span> $ans = &lt;STDIN&gt;;

  <span class="hljs-keyword">die</span>(<span class="hljs-string">"Aborted due to lack of consent to proceed\n"</span>)
    <span class="hljs-keyword">unless</span> ($ans =~ <span class="hljs-regexp">/^y/i</span>);
}

<span class="hljs-function"><span class="hljs-keyword">sub</span> <span class="hljs-title">translate</span> </span>{
  <span class="hljs-keyword">my</span> ($text, $lang) = @_;

  <span class="hljs-keyword">my</span> $ua = LWP::UserAgent-&gt;new;
  <span class="hljs-keyword">my</span> $url = <span class="hljs-string">'https://translation.googleapis.com/language/translate/v2'</span>;

  <span class="hljs-keyword">my</span> $res = $ua-&gt;post($url,
		      [
		       <span class="hljs-string">source =&gt;</span> <span class="hljs-string">'en'</span>,
		       <span class="hljs-string">target =&gt;</span> $lang,
		       <span class="hljs-string">format =&gt;</span> <span class="hljs-string">'html'</span>, <span class="hljs-comment"># Could be 'text'</span>
		       <span class="hljs-string">key =&gt;</span> $APIkey,
		       <span class="hljs-string">q =&gt;</span> $text,
		      ]);

  <span class="hljs-keyword">die</span>(<span class="hljs-string">"Failed to access server: "</span>. $res-&gt;status_line . <span class="hljs-string">"\n"</span>)
    <span class="hljs-keyword">unless</span> ($res-&gt;is_success);

  <span class="hljs-keyword">my</span> $data = $res-&gt;content;

  <span class="hljs-keyword">my</span> $json = from_json($data, { <span class="hljs-string">utf8 =&gt;</span> <span class="hljs-number">1</span> } );

  <span class="hljs-keyword">my</span> $translated;

  <span class="hljs-keyword">eval</span> {
    <span class="hljs-keyword">my</span> $d = $json-&gt;{data};
    <span class="hljs-keyword">die</span>(<span class="hljs-string">"Missing \"data\" entry\n"</span>) <span class="hljs-keyword">unless</span> (<span class="hljs-keyword">defined</span> $d);

    <span class="hljs-keyword">my</span> $tr = $d-&gt;{translations};
    <span class="hljs-keyword">die</span>(<span class="hljs-string">"Missing \"translations\" entry\n"</span>)
      <span class="hljs-keyword">unless</span> ((<span class="hljs-keyword">defined</span> $tr) &amp;&amp; (<span class="hljs-keyword">ref</span> $tr eq <span class="hljs-string">'ARRAY'</span>) &amp;&amp;
	     (<span class="hljs-keyword">ref</span> $tr-&gt;[<span class="hljs-number">0</span>] eq <span class="hljs-string">'HASH'</span>));

    $translated = $tr-&gt;[<span class="hljs-number">0</span>]-&gt;{translatedText};

    <span class="hljs-keyword">die</span>(<span class="hljs-string">"No translated text\n"</span>)
      <span class="hljs-keyword">unless</span> (<span class="hljs-keyword">defined</span> $translated);
  };

  <span class="hljs-keyword">die</span>(<span class="hljs-string">"Malformed response from server: $@\n"</span>) <span class="hljs-keyword">if</span> ($@);

  $translated =~ <span class="hljs-regexp">s/(&lt;\/(?:p|h\d+)&gt;)[ \t\n\r]*/"$1\n"/g</span>e;

  <span class="hljs-keyword">return</span> $translated;
}</pre>
<p>The substitution at the end of the translate() function adds a newline after each closing tag for a paragraph or header (e.g. &lt;/p&gt;, &lt;h1&gt; etc.) so that the HTML is more readable with a text editor. Otherwise it&#8217;s all in one single line.</p>
<h3>Protecting your money</h3>
<p>By obtaining an API key, you effectively give your computer permission to spend money. Which is fine as long as it works as intended, but a plain bug in a script that leads to an infinite loop or recursion, or maybe just feeding the system with a huge file by mistake, can end up with consequences that are well beyond the CPU fan spinning a bit.</p>
<p>So there are two protection mechanisms in the script itself:</p>
<ul>
<li>The script prompts for permission, stating how much it will cost (based upon <a rel="noopener" href="https://cloud.google.com/translate/pricing" target="_blank">$20 / million chars</a>).</li>
<li>It limits a single translation to 500k chars (to avoid a huge file from being processed accidentally).</li>
</ul>
<p>Another safety mechanism is to set up budgets and budget alerts. Go to Main menu (hamburger) &gt; Billing &gt; Budgets &amp; Alerts. Be sure to check &#8220;Email alerts to billing admins and users&#8221;. If I got it right, budgets don&#8217;t protect against spending, but only sends notifications. So I selected a sum, and enabled only the 100% threshold. It seems to make sense to check all the Discounts and Promotion options in the Credits part, which makes sure that the alert is given for the money to be spent by deducing all promotion credits.</p>
<p>On top of that, it&#8217;s a good idea to set quota limits: Go to Main menu (hamburger) &gt; IAM &amp; Admin &gt; Quotas. Set the filter to Translation to get rid of a lot of lines.</p>
<p>It&#8217;s also the place to get an accurate figure for the current consumption.</p>
<p>Enable the quota for &#8220;v2 and v3 general model characters per day&#8221;, which is the only character limit that isn&#8217;t per minute, and set it to something sensible, for example 2 million characters if you&#8217;re a modest user like myself. That&#8217;s $40, which is fairly acceptable damage if the computer goes crazy, and high enough not to hit the roof normally.</p>
<p>Also do something with &#8220;v3 batch translation characters using general models per day&#8221; and same with AutoML custom models. I don&#8217;t use these, so I set both to zero. Just to be safe.</p>
<p>There&#8217;s &#8220;Edit Quotas&#8221; to the top right. Which didn&#8217;t work, probably because I did this during the trial period, so quotas are meaningless, and apparently disabled anyhow (or more precisely, enabled to fixed limits).</p>
<p>So the way to do it was somewhat tricky (as it&#8217;s probably pointless): To enable a quota, right-click the &#8220;Cloud Translation API&#8221; to the left of the quota item, and open it in a new tab. Set up the quota figure there. But this description on how to do it might not be accurate for a real-life use. Actually, the system ignored my attempts to impose limits. They appeared on the page for editing them, but not on the main page.</p>
<h3>Supporting CJK in LaTeX</h3>
<p>I&#8217;m wrapping up this post with notes on how to feed LaTeX (pdflatex, more precisely) with Chinese, Japanese and Korean, with UTF-8 encoding, and get a hopefully reasonable result.</p>
<p>So first grab a few packages:</p>
<pre># apt install texlive-lang-european
# apt install texlive-lang-chinese
# apt install texlive-lang-korean
# apt install texlive-cjk-all</pre>
<p>Actually, texlive-lang-european isn&#8217;t related, but as its name implies, it&#8217;s useful for European languages.</p>
<p>I first attempted with</p>
<pre><span class="hljs-keyword">\usepackage</span>[UTF8]{ctex}</pre>
<p>but pdflatex failed miserably with an error saying that the fontset &#8216;fandol&#8217; is unavailable in current mode, <a rel="noopener" href="https://tex.stackexchange.com/questions/545681/critical-package-ctex-errorctex-fontsetfandol-is-unavailable-in-current" target="_blank">whatever that means</a>. After trying a few options back and forth, I eventually went for the rather hacky solution of using CJKutf8. The problem is that CJK chars are allowed only within</p>
<pre><span class="hljs-keyword">\begin</span>{CJK}{UTF8}{gbsn}

<span class="yadayada">[ ... ]</span>

<span class="hljs-keyword">\end</span>{CJK}</pre>
<p>but I want it on the whole document, and I need the language setting to be made in a file that is included by the main LaTeX file (a different included file for each language). So I went for this simple hack:</p>
<pre><span class="hljs-keyword">\AtBeginDocument</span>{<span class="hljs-keyword">\begin</span>{CJK}{UTF8}{gbsn}}
<span class="hljs-keyword">\AtEndDocument</span>{<span class="hljs-keyword">\end</span>{CJK}}</pre>
<p>As for the font, <a rel="noopener" href="https://www.overleaf.com/learn/latex/Chinese" target="_blank">it appears like</a> gbsn or gkai fonts should be used with Simplified Chinese, and bsmi or bkai for with Traditional Chinese. Since I translated into Simplified Chinese, some characters just vanished from the output document when trying bsmi and bkai. The back-translation to English of a document made with bsmi was significantly worse, so these dropped characters had a clear impact in intelligibility of the Chinese text.</p>
<p>I got this LaTeX warning saying</p>
<pre>LaTeX Font Warning: Some font shapes were not available, defaults substituted.</pre>
<p>no matter which of these fonts I chose, so it doesn&#8217;t mean much.</p>
<p>So the choice is between gbsn or gkai, but which one? To decide, I copy-pasted Chinese text from updated Chinese websites, and compared the outcome of LaTeX, based upon the TeX file shown below. It was quite clear that gbsn is closer to the fonts in use in these sites, even though I suspect it&#8217;s a bit of a Times New Roman: The fonts used on the web have less serifs than gbsn. So gbsn it is, even though it would have been nicer with a font with less serifs.</p>
<p>For Japanese, there&#8217;s &#8220;min&#8221;, &#8220;maru&#8221; and &#8220;goth&#8221; fonts. &#8220;Min&#8221; is a serif font, giving it a traditional look (calligraphy style) and judging from Japanese websites, it appears to be used primarily for logos and formal text (the welcoming words of a university&#8217;s president, for example).</p>
<p>&#8220;Maru&#8221; and &#8220;goth&#8221; are based upon simple lines, similar to plain text in Japanese websites. The latter is a bit of a bold version of &#8220;maru&#8221;, but it&#8217;s what seems to be popular. So I went with &#8220;goth&#8221;, which has a clean and simple appearance, similar to the vast majority of Japanese websites, even though the bold of &#8220;goth&#8221; can get a bit messy with densely drawn characters. It&#8217;s just that &#8220;maru&#8221; looks a bit thin compared to what is commonly preferred.</p>
<p>Korean has two fonts in theory, &#8220;mj&#8221; and &#8220;gt&#8221;. &#8220;mj&#8221; is a serif font with an old fashioned look, and &#8220;gt&#8221; is once again the plain, gothic version. I first failed to use the &#8220;gt&#8221; font even though it was clearly installed (there were a lot of files in the same directories as where the &#8220;mj&#8221; files were installed, only with &#8220;gt&#8221;). Nevertheless, trying the &#8220;gt&#8221; font instead of &#8220;mj&#8221; failed with</p>
<pre>LaTeX Font Warning: Font shape `C70/gt/m/it' undefined
(Font)              using `C70/song/m/n' instead on input line 8.

! Undefined control sequence.
try@size@range ...extract@rangefontinfo font@info
                                                  &lt;-*&gt;@nil &lt;@nnil</pre>
<p>But as it turns out, it should be referred to as &#8220;nanumgt&#8221;, e.g.</p>
<pre>\begin{CJK}{UTF8}{<span class="punch">nanumgt</span>}
나는 멋진 글꼴을 원한다
\end{CJK}</pre>
<p>It&#8217;s worth mentioning XeLaTeX, which allows using an arbitrary True Type font withing LaTeX, so the font selection is less limited.</p>
<p>See <a rel="noopener" href="https://tex.my/2010/06/21/cjk-support-in-latex/" target="_blank">this page</a> on fonts in Japanese and Korean.</p>
<p>For these tests, I used the following LaTeX file for use with e.g.</p>
<pre>$ pdflatex test.tex</pre>
<pre><span class="hljs-keyword">\documentclass</span>{hitec}
<span class="hljs-keyword">\usepackage</span>[utf8]{inputenc}
<span class="hljs-keyword">\usepackage</span>[T1]{fontenc}
<span class="hljs-keyword">\usepackage</span>{CJKutf8}
<span class="hljs-keyword">\newcommand</span>{<span class="hljs-keyword">\thetext</span>}
{

它说什么并不重要，重要的是它是如何写的。
}

<span class="hljs-keyword">\AtBeginDocument</span>{}
<span class="hljs-keyword">\AtEndDocument</span>{}
<span class="hljs-keyword">\title</span>{This document}
<span class="hljs-keyword">\begin</span>{document}

gbsn:

<span class="hljs-keyword">\begin</span>{CJK}{UTF8}{gbsn}
<span class="hljs-keyword">\thetext</span>
<span class="hljs-keyword">\end</span>{CJK}

gkai:

<span class="hljs-keyword">\begin</span>{CJK}{UTF8}{gkai}
<span class="hljs-keyword">\thetext</span>
<span class="hljs-keyword">\end</span>{CJK}

bsmi:

<span class="hljs-keyword">\begin</span>{CJK}{UTF8}{bsmi}
<span class="hljs-keyword">\thetext</span>
<span class="hljs-keyword">\end</span>{CJK}

bkai:

<span class="hljs-keyword">\begin</span>{CJK}{UTF8}{bkai}
<span class="hljs-keyword">\thetext</span>
<span class="hljs-keyword">\end</span>{CJK}

<span class="hljs-keyword">\end</span>{document}</pre>
]]></content:encoded>
			<wfw:commentRss>https://billauer.se/blog/2022/08/google-translate-pdflatex-technical/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Translating technical documentation with Google Translate</title>
		<link>https://billauer.se/blog/2022/08/google-translate-insights/</link>
		<comments>https://billauer.se/blog/2022/08/google-translate-insights/#comments</comments>
		<pubDate>Mon, 15 Aug 2022 07:06:15 +0000</pubDate>
		<dc:creator>eli</dc:creator>
				<category><![CDATA[Internet]]></category>
		<category><![CDATA[Software]]></category>

		<guid isPermaLink="false">https://billauer.se/blog/?p=6658</guid>
		<description><![CDATA[Introduction This post summarizes my insights as I worked my way through translating some technical documents, written in LaTeX, into Chinese, Japanese and Korean. The immediate approach was to feed Google Translate with the pdf documents, but not only are the results ugly, but then there are a lot of technical terms in the documents [...]]]></description>
			<content:encoded><![CDATA[<h3>Introduction</h3>
<p>This post summarizes my insights as I worked my way through translating some technical documents, written in LaTeX, into Chinese, Japanese and Korean. The immediate approach was to feed Google Translate with the pdf documents, but not only are the results ugly, but then there are a lot of technical terms in the documents which are better not translated. Even worse, there are code examples with explanation in the text, file names, references to variable names and other elements that become meaningless if translated.</p>
<p>One of the advantages of having the document written in LaTeX to begin with, is that the LaTeX text formatting commands effectively flag the parts that aren&#8217;t just plain language in the text, so it&#8217;s relatively easy to spot them and protect them. But that alone was a long way from the finish line, as elaborated in this unexpectedly long post.</p>
<p>A <a title="Google Translate, LaTeX and asian languages: Technical notes" href="https://billauer.se/blog/2022/08/google-translate-pdflatex-technical/" target="_blank">different post</a> discusses the technical aspects of talking with Google Cloud&#8217;s API as well as creating documents in these languages with LaTeX.</p>
<p>I also did something similar with translating web pages. For example, the translation of <a href="https://www.01signal.com/verilog-design/clockdomains/introduction/" target="_blank">this post</a> to <a href="https://www.01signal.com/zh/verilog-design/clockdomains/introduction/" target="_blank">Chinese</a>, <a href="https://www.01signal.com/ja/verilog-design/clockdomains/introduction/" target="_blank">Japanese</a> and <a href="https://www.01signal.com/ko/verilog-design/clockdomains/introduction/" target="_blank">Korean</a>.</p>
<p>This post was written in the summer of 2022, and odds are that things will change dramatically over the course of time.</p>
<h3>Is translation by human better?</h3>
<p>The short answer: Yes, as of 2023, human translation is much better. It&#8217;s mainly because there is no way to give the translating tool hints about the context. For example, the word &#8220;driver&#8221; could be a car driver or a term related to a computer. All translation tools just pick one meaning. Some tools allow choosing a specific dictionary, and there are ways to shape the behavior of the translator. But the results are far from satisfactory.</p>
<p>However but both options have their disadvantages: Working with a human necessarily requires trusting that a specific person will perform the job thoroughly, and well, that&#8217;s anything but taken for granted. It&#8217;s extremely difficult to verify that the work was done well, in particular when the document is technical, as it&#8217;s not possible to give it to just someone and ask if it&#8217;s well written. An automatic reverse translation will miss some poor translations (in particular poor translations of technical terms) and at the same time make false alarms.</p>
<p>But the worst problem with human translation is that every future change in text requires contacting the people who made the translation, and ask them to make the adjustments. They may not be so willing to do that. So unless you employ these people full-time, it may be difficult to translate small edits.</p>
<p>Another problem with humans is that significant errors in the meaning of the text might occur. It&#8217;s easy to reverse or otherwise obscure the meaning of a sentence because of a simple human error. &#8220;Be sure not to turn on the power supply&#8221; can easily turn into &#8220;Be sure to turn on the power supply&#8221;. Automatic reverse translation can reveal this, but it&#8217;s easy to miss an error like this, when the person that verifies the text already knows what it should say.</p>
<p>Automatic translation should be less likely to make a mistake of this sort, but the truth is that Google Translate, with all its Neural Network magic, turns out to be more human than desired in this matter: It&#8217;s not completely unusual that the meaning of the text changes, sometimes to the complete opposite.</p>
<p>It also has a variety of passive-aggressive behaviors, in particular ignoring whole sentences or part of them, mostly when the text becomes a bit rambling.</p>
<p>I had a case where the automatic translation ignored a &#8220;non-&#8221; prefix on a noun, and by doing so reversed the meaning of the sentence. I&#8217;ve also had a case where &#8220;must not&#8221; was translated into the equivalent of &#8220;doesn&#8217;t have to&#8221;.</p>
<p>The clear disadvantage of an automatic translation is poor expression and grammar. If the technique explained below is adopted, it&#8217;s however possible to end up with a fairly good result, even if the language is a bit off at times.</p>
<p>But this disadvantage can be mitigated by letting someone who knows the target language well proof-read the result. This person doesn&#8217;t need to know English well, but only be sensitive to the target language, so it&#8217;s easier to find someone for that job. And in particular when translating to Asian languages, it&#8217;s easy to tell the persons involved to ignore technical terms, as they are easily distinguishable, written in Latin script.</p>
<p>The results of this proof-reading session are only slight changes in word choice or ordering, and they can be verified against automatic translation as well as another person. In fact, in most cases, the best way is to improve the wording in the original language, until the person checking the text confirms it sounds OK.</p>
<p>Whether it&#8217;s worth the effort and cost to make this language cleanup is an open question. It&#8217;s a matter of how much the target audience appreciates the fact that the documentation is available in their language vs. how much the language defects come across badly.</p>
<p>Another issue with automatic translation is that words with more than one meaning can be mistranslated, in particular when the intended meaning is the less common one for a specific word (examples for that below). A back-translation doesn&#8217;t necessarily reveal a problem of this sort.</p>
<p>So with the possibility of having someone read through the translated text, the only remaining problem is when the meaning is changed unnoticed during the translation. Frankly speaking, I don&#8217;t know which option, human or machine, is better regarding this problem. The only real solution anyhow is to back-translate the text and read it through. Good luck with that.</p>
<h3>General insights on automatic translation</h3>
<p>Google Translate <a rel="noopener" href="https://en.wikipedia.org/wiki/Google_Neural_Machine_Translation" target="_blank">is based upon</a> a neural network machine learning algorithm, which means that it&#8217;s chaotic by nature (in the scientific sense). That gives it a bit of a human touch, which surely makes the translations better, but also makes it quite unpredictable. In particular, it&#8217;s capable of making any possible mistake, no matter how pointless and unexpected. It&#8217;s impossible to be 100% sure that it won&#8217;t do this or that, and it&#8217;s not even a bug when a phrase in the original text just disappears, or when a meaningless string of characters gets translated to something else, also completely meaningless. Those small glitches are part of the game, and it makes automated processing of the translated text quite challenging.</p>
<p>Having said that, the general rule is that if Google Translate does weird things, it&#8217;s because it was fed with something it found hard to digest. So even if the weirdness doesn&#8217;t appear to be related to language, the best way to rectify this is to change the original text into a simpler, more common way to express the same idea. Unfortunately, this calls for dull, play-it-safe English. However with by far less silly typos and grammar mistakes.</p>
<p>If I was to speculate how Google Translate&#8217;s algorithm works, I would say something like this: Attempt to find recognizable words in the sentence, fix spelling mistakes (&#8220;did-you-mean&#8221; style) and try to match the words that are recognized with a known phrase from the huge training corpus. Pick the known translation into the desired language of the word pattern that fits best. Fill in the words that were unknown in the original language in the translated text in their natural positions — these are treated as names (of persons, places etc.).</p>
<p>Punctuation like full period and commas, as well as enclosure in parentheses, makes the translator treat each part separately, more or less.</p>
<p>The destination language matters a lot regarding the interpretation of the meaning of the text. It doesn&#8217;t seem like the question is &#8220;what does the original text mean&#8221; but &#8220;which language pattern was most common in the training data for translating into language X&#8221;.</p>
<p>The main takeaways for this speculation, regarding how to write for translation are:</p>
<ul>
<li>Use common expressions and language patterns. In particular, use the most commonly used words to express a certain meaning.</li>
<li>Be super-careful with trivial spelling mistakes, as they break the statistics for the desired language pattern.</li>
<li>If the translation is successful to one language, in the sense that the original meaning was &#8220;understood&#8221;, it doesn&#8217;t necessarily means it will be as successful to another one. Same goes with failure. It seems to depend on what the translations between the two languages are usually used for. In other words, judging by the results, it seems like translations into Hebrew are used more for everyday text, but translation into east Asian languages is more for technical documents. Hence the selection of meaning tends to be more technical with the latter.</li>
<li>As there is no analysis of the semantics of the original sentence, anything can happen, including a translation that says the opposite of the original.</li>
</ul>
<p>Interestingly enough, I&#8217;m under the impression that the translation with Google Lens is much better than the cloud based translation service. In particular, the cloud translation is more likely to produce nonsense translations because of small disturbances in the middle of text, where Google Lens&#8217; translation seems to have extra wisdom to overcome such.</p>
<h3>Translating to a foreign language</h3>
<p>How do you know a translation is OK, when you don&#8217;t know the language it&#8217;s translated into? The short answer is that one can&#8217;t really know. It helps a lot knowing another language, even if it&#8217;s different from the target language, because it allows spotting misinterpretations of certain words, in particular technical ones. But often a word is poorly translated into one language, but fine with another.</p>
<p>There&#8217;s the possibility to translate it back to English, but that doesn&#8217;t always spot problems. Technical words like &#8220;clock&#8221;, &#8220;bus&#8221;, &#8220;sink&#8221;, &#8220;assertion&#8221; are translated to ridiculous words in Hebrew, for example, but the translation back looks OK in English. In particular a work like &#8220;sink&#8221; translates into the word in Hebrew that means kitchen sink, and then goes back to the correct work in English, of course.</p>
<p>But then comes the question: Why translate these words at all?</p>
<h3>Quality of translation</h3>
<p>Among the three target languages, the translation to (simplified) Chinese is the best by far. Probably because the natural flow of Chinese is the closest to western languages. The runner-up is Korean, and the worst is Japanese.</p>
<p>The worst problem with both Korean and Japanese is that parts of the original text can just disappear. This happens often when the semantic structure gets too complicated, or if there&#8217;s no normal way to say something in Japanese. For example, the sentence &#8220;you&#8217;re absolutely welcome to mess up completely, the tools won&#8217;t stand in your way&#8221; lost the entire first part in Japanese. So it just says &#8220;no tools get in the way&#8221;. If only the first part is translated separately, it turns into &#8220;completely ruined is welcome&#8221; (it had to give me something back when that sentence stood alone).</p>
<p>So short and plainly informative sentences are best translated into Japanese and Korean. Chinese seems to work with anything.</p>
<p>As for words like &#8220;it&#8221;, Chinese tolerates that best too. The two other languages are more likely to need repeating the word that &#8220;it&#8221; refers to, and hence possibly pick the wrong word to repeat.</p>
<h3>Testing by translating to Hebrew</h3>
<p>Since I happen to speak Hebrew fluently, I checked the translation to Hebrew of all documents, not for the purpose of publishing this translation, but because I soon found out that Google Translate struggles with Hebrew. So the idea was that if it&#8217;s OK with Hebrew, it&#8217;s probably OK any language.</p>
<p>For this, I tried two cases where the translation got wrong, as indicated by the result in Hebrew.</p>
<p>The first sentence that failed was &#8220;Close the window after a successful generation&#8221;. The problem was that the word &#8220;generation&#8221; was interpreted as the relationship between age groups, and not from the word &#8220;generate&#8221; as intended. This, in itself, is easily fixed by changing it into &#8220;Close the window after a successful generation <em>of the file</em>&#8220;. It was a matter of fitting the entire sentence into a different pattern of words.</p>
<p>Surprisingly enough, the translation into Chinese, Japanese and Korean was correct even without the fix. This can be verified by looking at the translation back to English, and isolate the word or couple of words of interest.</p>
<p>The next problematic phrase was &#8220;The non-X81X are ignored by almost all X82X computers&#8221;. In the translation to Hebrew, the &#8220;non-&#8221; part was ignored, so the sentence&#8217; meaning was reversed. Once again, the translation into the three other languages was correct (the X81X thing is explained below).</p>
<p>So if I once had the speculation that the machine translates the words into an intermediate format that somehow contains the meaning, and takes it into the target language from there, it&#8217;s definitely not the case. Whether there&#8217;s a misunderstanding or not in the translation depends on the target language.</p>
<p>I&#8217;m optimistic and hope that Hebrew is in particular prone to errors, so if I clean up the translation to Hebrew, it will hopefully work with other languages. However odds are that each language has its own pitfalls. Even though it really seems like the translation to Hebrew from any language is bad in particular. Including changing the meaning of the text. Also, I&#8217;ve found that plain idioms like &#8220;it doesn&#8217;t hurt&#8221; are often translated horribly to Hebrew but get perfectly OK in CJK languages. But then, I don&#8217;t know about misses in CJK languages that were OK in Hebrew&#8230;? And yet, after checking numerous expressions (&#8220;bite back&#8221;, &#8220;copy-paste&#8221; and a lot of this sort) it really seems like Hebrew is really bad off.</p>
<p>This way or another, the only sure benefit of checking the translation to Hebrew is that it does, after all, remove some ambiguities, whether that is necessary or not. Actually, I found tons of plain typos by looking at this translation, so that alone justifies this phase. It&#8217;s difficult to proofread text exactly as it was written, but reading it again in another language feels as if someone else wrote it.</p>
<p>I also had the opportunity to have a translation into Japanese by a helpful person, and it was quite clear that the problems were in the places where the Hebrew translation also limped.</p>
<h3>Hands-on insights</h3>
<p>After quite some back and forth, I learned that the best way to work with Google Translate with text is to feed it with paragraphs of text in HTML, enclosed in &lt;p&gt; (or &lt;hN&gt;) tags. Plain formatting tags is fine (&lt;b&gt;, &lt;i&gt; and even &lt;span&gt; etc.) but it&#8217;s important not to insert anything that breaks the continuity of the sentences: No &lt;br&gt; or &lt;img&gt; tags in the middle, or anything else that isn&#8217;t expected in the middle of a sentence. It makes Google Translate translate the part before and after the break as separate sentences, and that&#8217;s a disaster.</p>
<p>Newlines are ignored in the cloud interface with HTML, as they should be. This is contrary to the web interface for Google Translate, which is extremely sensitive to newlines, so copy-pasting a chunk of text from a pdf document can result in a horrible translation, because there are newlines between each row in the original text, which makes the translator treat each row a separate phrase.</p>
<p>But the real difficulty is the fact that the translated text is technical. Google Translate is trained with mainly non-technical text (I guess), so its interpretation of technical terms that happen to also have a non-technical meaning is naturally inclined towards the non-technical meaning. Words like &#8220;driver&#8221;, &#8220;compile&#8221;, &#8220;assert&#8221; and &#8220;board&#8221; are not only likely to be translated incorrectly, but also stir a mess in that imaginary brain that holds all those neurons, resulting in a completely unintelligible translation.</p>
<p>The problematic words are those that have a possible non-technical meaning. The word &#8220;boot&#8221; could mean a piece of footwear, to boot a computer <em>could</em> be mistaken for &#8220;to give the computer the boot&#8221;, but to <em>reboot</em> a computer could only mean one thing. So it&#8217;s not all that much about the word being technical, like the fact that it could be remotely confusing.</p>
<p>Other ambiguities occur with words like &#8220;target&#8221;. Using it in any form, i.e. &#8220;to target&#8221; or &#8220;targeting&#8221; as well as &#8220;targeted&#8221; as in &#8220;depending on which software version is targeted&#8221; leads to a completely wonky translation, at least into Hebrew.</p>
<p>Surprisingly enough, it copes quite well with sentences that contain untranslatable items. I guess it treats anything it can&#8217;t handle as a name. Since it&#8217;s supposed to be able to translate &#8220;Joseph prefers Paris over Berlin&#8221;, it works fine with &#8220;X prefers Y over Z&#8221; as well. So the trick is to remove all technical terms from the the text, and replace them with something that Google Translate will treat as a name, something it can&#8217;t translate. And then return those words into the translated text.</p>
<p>This means that all technical terms remain in English in the translated text, which is perfectly fine, because a technical reader is expected to know these terms. It&#8217;s the blah-blah part that needs translation, and with the technical words out of the way, Google Translate does a good job on that.</p>
<p>The problem that remains is how to feed the translator with these untranslatable X, Y and Z <em>placeholders</em>, when there can be thousands of these, and they must all be left intact in the translation (well, except for Russian and Greek, see below). The section below on placeholders tells the full story, but the spoiler is that I used X0X, X1X, X2X, and so on. It&#8217;s not watertight, but it works best. I tried quite a few options.</p>
<p>The main thing to keep in mind is that <strong>it&#8217;s all about word patterns</strong>: If Google Translate recognizes the structure of the sentence, based upon words that are commonly used together for a certain meaning, it translates that part correctly, and then puts the placeholders in the right places, treating them as names.</p>
<p>I should mention that Google Translate offers a &#8220;notranslate&#8221; style, which can be used to enclose e.g. &lt;span&gt; segments of text that shouldn&#8217;t be translated. I didn&#8217;t attempt using it, in particular as people out there in the web have complained that it disrupts the translation exactly like that. Another problem is that chunks that shouldn&#8217;t be translated often have a different formatting (e.g. Courier font for variable names), and Google Translate tends to behave in an unpredictable manner, making it difficult to rely on its output for feeding LaTeX with directly.</p>
<p>Also worth mentioning is that Google offers an advanced version of the translation API, with the ability to train the learning machine and possibly feed it with specific word translations, but that would require knowing the correct term in the target language. How do you say &#8220;compile&#8221; in Chinese and Japanese? But it could have been helpful for solving the problem with verbs, that have a technical meaning (&#8220;compile&#8221;, &#8220;boot&#8221;, &#8220;implement&#8221;, &#8220;overflow&#8221;, you name it).</p>
<h3>How I actually did it</h3>
<p>The idea was to extract translatable text from the LaTeX source, and feed Google Translate&#8217;s cloud API with it in HTML mode. Then take the translated text and implant it back into the LaTeX doc.</p>
<p>The overall goal is to feed Google Translate with hollow phrases, albeit with a solid and common semantic structure, of the form of &#8220;X with Y is more important that Z&#8221;. This makes it easy for the translator to detect the structure of the phrase, and translate it to a meaningful sentence in the foreign language. That gives good odds for meaningful sentence when the placeholders are replaced with the actual technical words in the translated phrase.</p>
<p>In more detail:</p>
<ul>
<li>Fetch paragraphs of text and enclose them in &lt;p&gt; or &lt;h1&gt;, &lt;h2&gt; or &lt;h3&gt; tags. Each of these tags have a unique &#8220;id&#8221; attribute, so when the translation returns, it&#8217;s possible to track which text segments should be written back to which part in the LaTeX file. This is why HTML mode came handy. I haven&#8217;t had a single case of these attributes being messed up (yet?).</li>
<li>Turn some LaTeX formatting into plain HTML tags, e.g. &lt;b&gt;, &lt;i&gt; etc. Then do the opposite when implanting the text back. The advantage is that this doesn&#8217;t break Google Translate&#8217;s view of the text as a contiguous sentence. Once again, HTML mode is required for this stunt.</li>
<li>Anything that shouldn&#8217;t be translated — technical terms, references to variables, file names, references to paragraphs, labels etc. — is replaced with a unique identifier (&#8220;placeholder&#8221;) that Google Translate doesn&#8217;t attempt to translate. The major caveat with this method is that it works only with nouns. This requires rewording, in particular turning verbs into nouns (e.g. &#8220;perform a compilation&#8221; instead of &#8220;compile&#8221;). More on this below.</li>
</ul>
<p>Note that some parts of the LaTeX document are completely out of this game, as they aren&#8217;t even given to the translator to look at. For example, verbatim environment chunks, and even the newlines between the text paragraphs. They remain the same because they aren&#8217;t overwritten when the translated text is transformed back and implanted in the relevant segment.</p>
<h3>Work flow</h3>
<p>I wrote a Perl script for the back-and-forth manipulations between LaTeX and HTML, but I won&#8217;t get into that too much, because it&#8217;s complicated and really specific to the documents at hand for translation. Among others, this script loaded a list of words that are always replaced with placeholders, and I also added a LaTeX command, \notranslate{}, which just leaves the content as is when interpreted by LaTeX, but to the script it means that the entire chunk should be replaced with a placeholder as well.</p>
<p>Writing scripts and all that is nice, but there&#8217;s still some manual preparation required. So this was the procedure I adopted:</p>
<ul>
<li>Run the script that creates the HTML file that is sent to Google Translate. View that file with a web browser, and look for words that are technical and can be mistranslated. When such are found, either add the word or phrase to the list of words to automatically replace with placeholders, or fix it specifically with \notranslate{} LaTeX statements.</li>
<li>In fact, I also wrote a script that puts \notranslate{} on certain words and patterns (e.g. sets of upper case characters) so I ran this script, and then verified each such occurrence. This is faster than finding them manually, and is useful for words that may have a non-technical meaning, or otherwise require human attention to get 100% right. For example, the word &#8220;image&#8221; should be translated when it refers to a picture in the text, but not when it&#8217;s an image of a disk.</li>
<li>Go through the text manually, and apply the guidelines listed below (the do&#8217;s and don&#8217;ts).</li>
<li>Translate the text into Hebrew, and read through the result. If something ends up unclear, fix it. The further the language is from English, the better. The one sure benefit of this check is that small typos are spotted (e.g. &#8220;in&#8221; instead of &#8220;is&#8221;) because the translation gets weird. The fact that the order of words changes in the translation also helps spotting ambiguities, that are often solved with works like &#8220;which is&#8221; or punctuation.</li>
<li>Translate into the target language. Make the necessary fixes. Don&#8217;t bother to find out why certain placeholders are missing in the translation. Rather, look at the original text and try to figure out why it was difficult to translate, and fix that instead. Sometimes a missing placeholder is due to a whole sentence being dropped off, in particular with Korean. It&#8217;s as if the algorithm said &#8220;I have no idea how to reorganize this sentence into something that makes sense in Korean, so I&#8217;ll just skip it&#8221;.</li>
<li>Maybe attempt to translate the document back as a pdf file (with Google Translate&#8217;s web interface) or use Google Lens&#8217; translation feature for a more sporadic check. I&#8217;m not sure if this is worth the time.</li>
</ul>
<p>The order of translation is Korean first, then Japanese and finally Chinese. This is because the translation to Korean is the most troublesome, however often fixing the problems consists of changes that are likely to benefit the other translations.</p>
<p>All in all, it appears like using placeholders instead of technical terms actually improved the translation regardless of these specific words. It seems like these words confused the translation machinery, which made it create obscure phrasing. With the technical words out of the way, inserted as opaque symbols, it seems like Google Translate managed much better to handle the rest, which now consisted of commonly spoken language.</p>
<p>So my ultimate approach was to put placeholders instead of virtually all technical terms which are nouns. That&#8217;s quite a lot of them, and the translated documents ended up full with terms in English. I&#8217;m not sure what Chinese are going to think about this, but if they have the same problem as in Hebrew — weird &#8220;official words&#8221; for technical terms — it&#8217;s going to be just fine.</p>
<h3>The do&#8217;s and don&#8217;ts</h3>
<p>Based upon quite some trial and error, these are the guidelines I ended up with for producing text with placeholders that translates well.</p>
<ul>
<li>The text should consist of hollow sentences like &#8220;If the X113X process fails, the X641X&#8217;s output may supply hints on what went wrong. The X102X application on the computer should be configured for X114X, no X115X and no X116X ( X640X )&#8221;. However sentences like &#8220;The X219X for X220X on X221X or X222X is part of the majority of X223X and distributions, as explained below&#8221; should be fixed, inserting some meaningful words between those four placeholders with just one word between each. In this example, it&#8217;s not clear whether the last &#8220;or&#8221; refers to instead of X221X alone or all of the three. If the translation requires word reordering, this will obscure the meaning of the sentence.</li>
<li>Use punctuation (in particular commas) and parentheses to chop up long sentences into segments. This prevents ambiguity. In particular, text inside parentheses is translated into parentheses, so this is a good tool for breaking up long and complicated sentences.</li>
<li>Try to keep phrases short and concise (and somewhat boring), partly because sentences are short in the target languages. If the sentence is long, try to mitigate the damage with punctuation.</li>
<li>Use plain and explicit English. Don&#8217;t leave out &#8220;which&#8221;, &#8220;that&#8221; and all those words that explicitly define the role of each word. Even a simple expression like &#8220;for the curious&#8221; can go wrong, but works perfectly well when changed into &#8220;for those who are curious&#8221;. Yuck, but translates well.</li>
<li>Avoid words that refer back to something earlier in the sentence, unless it&#8217;s very obvious. In particular, the word &#8220;it&#8221; is often replaced with the word it&#8217;s supposed to refer to during the translation, and sometimes the wrong word is chosen in the translation. When this happens, the translation explicitly changes the meaning. Because the translation into CJK languages often involves splitting a long sentence into shorter ones, without a possibility to use a word like &#8220;it&#8221;, implicit references of this sort easily translate into nonsense. To make things worse, the back-translation may bring back the &#8220;it&#8221;, so there&#8217;s no way to spot the mistaken translation. There are cases where these duplications are safe, for example expressions like &#8220;one thing to another&#8221; (which is often translated into &#8220;one thing to another thing&#8221;).</li>
<li>Prefer &#8220;the red book and the blue book&#8221; over &#8220;the red and blue books&#8221;. The order of the words may be changed during the translation, and in that case, it&#8217;s possible that only the &#8220;blue books&#8221; is moved to the new position in the sentence, so the result is rubbish. These overly explicit sentence are less awkward to read than they are awkward to write, but they are nevertheless slightly awkward as the same word is repeated over and over again.</li>
<li>Avoid idioms. Even the simplest ones, like &#8220;out of the box&#8221; may and may not translate into something that makes sense. Because of the statistical nature of the translations, idioms might get translated with the right spirit into a certain language, and fail completely with another. So dull language it is.</li>
<li>Avoid verbs in passive form, in particular if it comes with a &#8220;by&#8221;. Passive form is useful for not naming the doer, but if it&#8217;s named anyhow, use the active form. A lot of times, the passive form, and the tangled sentences that it usually creates, were the reason for problems in translation.</li>
<li>Use possessive form for clarification. For example, if the word &#8220;register&#8221; is replaced with a placeholder, &#8220;register modification&#8221; should change to &#8220;modification of registers&#8221; or likewise &#8220;registers&#8217; modification&#8221;. Using the &#8216;s suffix works great, so use it as long as it doesn&#8217;t create an ambiguity on who the owner is.</li>
<li>In fact, there&#8217;s no problem at all with segments like &#8220;X400X&#8217;s X401X&#8221;, as possessive form. This translates well, surprisingly enough.</li>
<li>Don&#8217;t replace partial expressions with placeholders. For example, in the expression &#8220;the user-space application&#8221;, don&#8217;t replace just &#8220;user-space&#8221;, but rather &#8220;user-space application&#8221;. Word ordering might be different in another language, which can at worst lead to a complete disassociation between the placeholder and its related word in English, with a completely unclear result.</li>
<li>Avoid replacement of parts of expressions with placeholders. For example, in &#8220;VGA port&#8221;, if only &#8220;VGA&#8221; is replaced, it&#8217;s not sure if this will translate fine. &#8220;VGA-port&#8221; increases the changes. If it&#8217;s a common language pattern, e.g. &#8220;VGA-based&#8221;, there&#8217;s a good chance for proper translation. Same goes with &#8220;the X500X file&#8221;, because it&#8217;s a common language pattern.</li>
<li>Don&#8217;t use &#8220;non-&#8221; as a prefix. It&#8217;s often missed, reversing the meaning.</li>
<li>Look out for ambiguous words. For example, the word &#8220;signals&#8221; could be the verb (to signal) but also the plural of the noun. Avoid less common uses of words, such as &#8220;writes&#8221; to say several write operations, and use &#8220;write operations&#8221; instead.</li>
<li>Be extra careful with trivial spelling mistakes and typos, in particular mixing &#8220;is&#8221; with &#8220;it&#8221; and such. These are overlooked when reading the text in English, but they kill the translation, sometimes by changing the meaning significantly, and sometimes by just confusing the translation algorithm into producing something weird.</li>
<li>Bonus: Check all duplication of placeholders, and verify that the correct one is duplicated. Because these duplications are usually the result of a word that refers back to something (&#8220;which&#8221;, &#8220;that&#8221;, &#8220;it&#8221; etc.), it&#8217;s a good idea to verify that the reference goes to the correct placeholder. In theory, this should be done with all uses of back referencing, but that means proofreading the entire text. So with placeholders it&#8217;s less work (and less gain). Having run through a checkup of my own translations, I&#8217;d say about 10% of these duplications garble the meaning, by explicitly duplicating the wrong word.</li>
</ul>
<h3>Caching translation results?</h3>
<p>Since the document is chopped into paragraphs, each within a &lt;p&gt; enclosure, does it matter if each is sent separately or if all are sent in one API transaction as a concatenated string? Does it matter if the translator sees the entire text?</p>
<p>Because if each &lt;p&gt; enclosure is treated separately, it&#8217;s possible to cache the pieces of text that have already been translated.</p>
<p>Caching is more than just a money saver. It allows making manual changes in Google Translate&#8217;s output (in particular if it messed up the placeholders) and then not having to repeat this every time the entire document is translated.</p>
<p>Even more important, avoiding the repeated translation of parts that have already been translated means avoiding the possible mishaps that may suddenly occur (like suddenly dropping a sentence). Think about making a small change, and then the translation fails on something completely different. But it worked last time!</p>
<p>This is also important if there&#8217;s feedback from readers that corrects a poor translation at a specific place. So caching is very helpful for the incremental kind of work that is necessary to maintain the document in the long run.</p>
<p>So I tried this with translating from English to Hebrew, and a bit with Chinese as well (by looking at the translation back to English). As it turns out, there are occasional differences between the translation of an isolated paragraph and that made with a context. But it doesn&#8217;t seem like an intelligent use of the context. Comparing the results, the isolated translation was sometimes better, sometimes worse, with a very slight difference in most cases. So it looks more like the algorithm randomly picked another wording, for no apparent reason. It was usually exchanging equally valid synonyms, or choosing to translate the name &#8220;Linux&#8221; to Hebrew or not.</p>
<p>Another observation I made is that the use of context is poor. For example, the word &#8220;call&#8221; is translated to the the word in Hebrew that means a phone call, but &#8220;function call&#8221; is translated correctly. So what if there&#8217;s a sentence saying something about a &#8220;function call&#8221;, and a sentence afterwards uses the word &#8220;caller&#8221;? In the &lt;p&gt; enclosure, that is. Well, the translation of &#8220;caller&#8221; still relates to a phone call. The neural network clearly didn&#8217;t learn anything from the first sentence.</p>
<p>So it makes perfect sense to cache translations at a paragraph level. If the original document changes, request a translation only on the enclosure that actually changed.</p>
<h3>Finding the right kind of placeholder</h3>
<p>This is a long explanation on why ended up with the XnX placeholders. I would skip this part if I were you.</p>
<p>As mentioned above, the main problem with translating a technical document is that some technical terms are translated into an unhelpful, sometimes ridiculous way, and that it confuses the translation algorithm. As the reader of the document is most likely familiar with the English term, it&#8217;s safer to leave these words as is. The problem is how to insert these terms in a way that ensures they don&#8217;t get translated, and at the same time retain their position in the context.</p>
<p>As it turned out, the main problem with inserting an untranslated chunk into the text is that it may disrupt the translation, in particular as Google Translate tends to treat the part before and after the chunk as separate sentences, which results in a poor translation that misses the point of the sentence.</p>
<p>I began with adding markers in a plain text (like &lt;%103%&gt;, [^29^] and ^^26^^), however Google Translate inserted a space in the middle of some of these (so it turned out to be e.g. &#8220;&lt; %103%&gt;&#8221;) and also threw in some markups where they shouldn&#8217;t be. A complete disaster, in short. This could have worked with non-HTML translation, but well, it didn&#8217;t work.</p>
<p>Another attempt was to use translation of HTML, with &lt;b id=&#8221;n23&#8243;&gt;P&lt;/b&gt; markers as placeholders. The id allowed to identify which placeholder to insert, and the &#8220;P&#8221; to give the translator something to consider as a word. This failed as well, in many ways: The fact that the &#8220;P&#8221; part sometimes got translated into &#8220;PP&#8221; (why on earth) didn&#8217;t matter much, because it&#8217;s not really important. The real problem was that at times there were other words inserted into the &lt;b&gt; enclosure as well (for no apparent reason). Even worse, sometimes a completely different word, somewhere else in the sentence, got into a &lt;b&gt; enclosure with the same id. So processing this would have been complicated.</p>
<p>Another thing I tried was to use &lt;var&gt;n&lt;/var&gt; enclosures, where n is the number of the placeholder. That failed, partly because some of these disappeared for no clear reason, and others were manipulated (for example, characters from previously outside the enclosure went into it).</p>
<p>To ensure that the placeholder is fully opaque, I tried &lt;img id=n23&gt;. The clear advantage was that Google Translate didn&#8217;t duplicate these not modify them, but they broke the sentence into fragments. Google Translate assumed that no sentence will have an image in the middle of it.</p>
<p>So if not images, what about emoticons? Or even better, I made an attempt to use the Unicode range U+13000 to U+1342e (Egyptian Hieroglyphs) as placeholders instead of &lt;img&gt; markups. The idea was that Google Translate would have to pass them through as is, and that they would be considered to be names. In order to make this work, there had to be a whitespace on both sides of the Hieroglyph, but even with that, Google Translate would mess up and occasionally add completely unrelated characters instead.</p>
<p>In the end, I went for inserting words like X0X, X1X, X2X, and so forth. These remain intact through translation, however they are occasionally duplicated, in particular with sentences like &#8220;that is possible with X, which is the best option&#8221; which can turn into &#8220;that is possible with X, and X is the best option&#8221;. The word &#8220;it&#8221; is also translated sometimes into the placeholder instead. But that&#8217;s actually a correct translation, and it&#8217;s easy to process. Even though this worked almost flawlessly, there were occasional surprises, including rare cases where Google Translate changed the number between the Xs without myself being able to figure out why on earth, and why that specific change. So there&#8217;s always a certain amount of manual cleanup after the translation.</p>
<p>These duplications are common with east Asian languages, and usually occur when a long sentence is chopped into several shorter ones. In these languages, it&#8217;s more common to repeat the word than to use &#8220;it&#8221;, &#8220;which&#8221; and such.</p>
<p>When translating to Russian and Greek, the &#8220;X&#8221; character was occasionally replaced with the Russian capital letter &#8220;Ha&#8221; (Unicode U+0425) or the Greek capital letter &#8220;Chi&#8221; (Unicode U+03A7). Both look exactly like an &#8220;X&#8221;, so the replacement is understandable. Once this issue is known, it&#8217;s quite easy to handle, so it&#8217;s not a big deal.</p>
<p>As for the quality of the translation, this worked well, and Google Translate combined these nicely into the translation, even when changing the word ordering was necessary. This works however only when the placeholder is used as a noun. So it doesn&#8217;t solve the problem with verbs like &#8220;assert&#8221;, &#8220;raise&#8221;. In some cases, a word like &#8220;overflow&#8221;, used as a verb, can be replaced with something like &#8220;cause an overflow&#8221;, so it can be translated properly.</p>
<p>Another thing with these XnX placeholders is that there must be a whitespace in either side of it, or Google Translate gets confused. To ensure that the placeholder is restored properly, the strategy was to include any surrounding whitespaces in the string that was stored to replace the placeholder later on, and then add a whitespace in either side of the XnX string. When reverting the process, all whitespaces around the XnX string were removed before restoring the original string. This results in a perfectly consistent back-and-forth, even if the translator adds or removes whitespaces (which happens a lot).</p>
<p>As a side note, Google charges for <a rel="noopener" href="https://cloud.google.com/translate/pricing#charged-characters" target="_blank">all characters</a>, even those not translated. Hence it&#8217;s a good idea to keep the placeholders short markups. Not a big deal, but still.</p>
<h3>Sanity checks on placeholders</h3>
<p>The natural expectation is that any placeholder in the text for translation will result in a single placeholder in the translation. I&#8217;ve already mentioned above that some placeholders turned into two in the translated text, and it was actually correct. But what if the placeholder disappears?</p>
<p>The answer is that it&#8217;s always an error, and it has to be fixed manually. In fact, it&#8217;s often an indication that something worse happened, which would have been left unspotted had it not been for the missing placeholder. Sometimes the number between the Xs is changed arbitrarily, but it happens in conjunction with other placeholders in the vicinity being messed up.</p>
<p>Sometimes the absent placeholder was the result of a part of a sentence that was completely eliminated. The small piece of information it contained was simply absent in the translation. This can happen for several reasons, but the most recurring one seems to be when it&#8217;s not clear what &#8220;which&#8221; or &#8220;that&#8221; refers to, earlier in the same sentence. One can get away with that in translations to European languages, but because the sentence is built differently in east Asian languages, the translator is forced to make a pick. So instead of doing that, it just eliminates the part it can&#8217;t decide upon. A neural network algorithm showing a bit of human behavior, I would say.</p>
<p>It also seems that a colon sign (&#8216;:&#8217;) tends to eliminate what comes immediately after it, fully or partly. Changing it to a full stop often returned chunks of texts from the dead in Korean and Japanese. Or splitting the text, so that part after the colon is in a separate enclosure (note to self: possibly with a \skipthis{}).</p>
<p>Same thing with a sentence starting with &#8220;likewise&#8221;.</p>
<p>Another somewhat weird phenomenon with Korean and Japanese is that a whole sentence was sometimes dropped. The really weird thing was that when the same sentence was put in a separate &lt;p&gt; enclosure, it was translated properly. So it was like Google Translate said &#8220;nah, this is too much rubbish, I&#8217;ll drop the last sentence&#8221;.</p>
<p>So in this sense, the placeholders help spotting other problems with the translation. I got an error of this sort for each few thousand translated words, which practically means a bit of fixing for each document. What&#8217;s really worrying is how many sentences without any placeholders have vanished unnoticed?</p>
<h3>Placeholders that contain a word in plural</h3>
<p>One problem that is inevitable with placeholders is that the information on the word&#8217;s plural vs. singular form is hidden away from the translator. So if the work that is hidden is &#8220;compilers&#8221;, the surrounding text in the translation might refer to it in singular, and that makes the sentence sound a bit off.</p>
<p>In some cases, the translator can deduce it from the surrounding words (e.g. if &#8220;is&#8221; or &#8220;are&#8221; is used in reference to it), but sometimes there are no hints. Luckily, the plural-singular thing isn&#8217;t very present in Chinese, Japanese and Korean, so the effect of this ambiguity is expected to be small. Try, for example to translate and back-translate &#8220;He gave me the books&#8221; with these languages, and you get &#8220;he gave me a book&#8221; — the indication for plural is lost. But there&#8217;s also a backside to this: The fact that the original word in English appears in its plural form will probably feel uneasy to an East Asian reader. I&#8217;m not sure about this, but it appears like they would use the English word in singular form anyhow, even if it refers to several pieces of whatever it is. So any use of plural will probably feel wrong to them.</p>
<p>Surprisingly, this can be fixed by using a placeholder like X205Xs (with the &#8220;s&#8221; in the end). This appears to be translated correctly into plural, and even the possessive form (e.g. X205Xs&#8217;) seems to work well into Hebrew.</p>
<p>But this hack creates a new problem: The translation might add suffixes and other grammatical elements to mark the plural form of the hidden word. If this happens, there will create a double plural. In German, for example, there are many ways to go from singular to plural, so this extra &#8220;s&#8221; just remains, when it comes after an XnX placeholder. If it isn&#8217;t removed, the result is &#8220;compilerss&#8221; (with a double &#8220;s&#8221; at the end). In Norwegian, it may add &#8220;-er&#8221; for plural (with the dash).</p>
<p>OK, so remove anything alphanumeric that comes after a placeholder, so that if the &#8220;s&#8221; remains, it&#8217;s gone? That may not work well either. For example, the possessive form in Swedish is expressed with a &#8220;:s&#8221; suffix and &#8220;:n&#8221; in Finnish (at least on a placeholder), so removing suffixes blindly takes its toll as well.</p>
<p>So even though appealing, there &#8220;s&#8221; method won&#8217;t work as a clean way to hint that the word is plural, in particular because the placeholder might get conjugated into plural in the translation. And there&#8217;s no catch-all solution for getting rid of this possible conjugation.</p>
<p>Given that the problem with plural is a relatively minor nuisance, that happens only when the context doesn&#8217;t say that it&#8217;s plural, it&#8217;s not worth the risk of adding garbage characters, or mistakenly removing relevant conjugation characters.</p>
<p>On the wishlist: The possibility to tell the translator that a blob is a noun in plural. Actually, wouldn&#8217;t it be nice to be able to do that with verbs as well, saying which tense and person?</p>
<h3>Placeholders and Korean particles</h3>
<p>In English, we have this thing that we say &#8220;a book&#8221; and &#8220;an orange&#8221;. The choice of the indefinite article, &#8220;a&#8221; or &#8220;an&#8221;, depends on whether the word that comes after it starts with a vowel or consonant sound.</p>
<p>In Korean, there are particles that are added after a noun to mark if it&#8217;s the subject, the topic or the object in the sentence. The particle is chosen according to whether the preceding word ends with a consonant or a vowel, respectively:</p>
<ul>
<li>Topic particles: 은 or 는 (eun or neun)</li>
<li>Subject particles: 이 or 가 (i or ga)</li>
<li>Object particles: 을 or 를 (eul or leul)</li>
</ul>
<p>Not surprisingly, the particles that come after a vocal begin with a consonant, so there&#8217;s always a consonant in the game. Same principle as English&#8217; indefinite article.</p>
<p>And here&#8217;s the crux: When a placeholder is used instead of a noun, Google Translate gets XnX instead of the real word, so the particle is chosen according to the &#8220;word&#8221; at hand.</p>
<p>So &#8220;I read the book&#8221; is translated by Google to 난 책<span class="punch">을</span> 읽는다 (book is 책, chaeg, ends with a consonant, hence the choice of the object particle 을, eul). But if &#8220;book&#8221; is replaced with &#8220;X10X&#8221;, we get 나는 X10X<span class="punch">를</span> 읽었다. &#8220;X&#8221; sounds like &#8220;eksae&#8221; in Korean, so it ends with a vowel, hence the 를 particle was used. (The word that means &#8220;I&#8221; changed from 난 to 나는, but the former is just a contraction of the latter, so it&#8217;s like &#8220;I&#8217;m&#8221; vs. &#8220;I am&#8221;)</p>
<p>This can be fixed automatically by looking for these particles: They are always immediately after a placeholder, and there&#8217;s a whitespace after them. The tricky part is to identify whether the replaced word ends with a consonant or a vowel, the way it&#8217;s pronounced in Korea (which may be different from the English pronunciation?).</p>
<p>The possessive particle, 의, as well as several other particles are indifferent to this matter.</p>
<p>It doesn&#8217;t seem like there&#8217;s a similar problem with Japanese nor Chinese, but I reached that conclusion based upon not finding anything related with a Google search. I will be really surprised if there was anything like this in Chinese because its script is generally unrelated to pronunciation. But with Japanese, I&#8217;m not that sure.</p>
<h3>Maybe use a word in the target language?</h3>
<p>I haven&#8217;t experimented a lot on this option, but maybe it will work: If a text is translated into Hebrew, and there is a Hebrew word in the middle of the text, it&#8217;s used correctly in the translation. So for example, &#8220;I ran back to בית quickly&#8221; is translated to &#8220;רצתי בחזרה לבית במהירות&#8221;. This isn&#8217;t perfect (הביתה would have been better) but it shows that a word in Hebrew is conjugated slightly and correctly.</p>
<p>So this opens for the possibility to replace technical terms with their relevant word in the target language. It seems like the grammar in CJK languages is exceptionally forgiving regarding nouns: There is generally no plural form, and it also seems like other conjugations are made with separate words (e.g possessive form).</p>
<p>Even more interesting, it works with verbs as well. &#8220;I רץ back to בית quickly&#8221; translated into &#8220;אני חוזר מהר לבית&#8221; which means &#8220;I quickly return home&#8221;. The word for &#8220;run&#8221; (רץ) was magically replaced with &#8220;return&#8221;, which is an interesting interpretation.</p>
<p>So maybe this can work. Not sure how much it improves, though.</p>
]]></content:encoded>
			<wfw:commentRss>https://billauer.se/blog/2022/08/google-translate-insights/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>Octave: Creating images from plots for web page</title>
		<link>https://billauer.se/blog/2021/08/octave-export-plot-to-png/</link>
		<comments>https://billauer.se/blog/2021/08/octave-export-plot-to-png/#comments</comments>
		<pubDate>Thu, 12 Aug 2021 16:07:00 +0000</pubDate>
		<dc:creator>eli</dc:creator>
				<category><![CDATA[Linux]]></category>
		<category><![CDATA[Software]]></category>

		<guid isPermaLink="false">https://billauer.se/blog/?p=6379</guid>
		<description><![CDATA[This should have been a trivial task, but it turned out quite difficult. So these are my notes for the next time. Octave 4.2.2 under Linux Mint 19, using qt5ct plugin with GNU plot (or else I get blank plots). So this is the small function I wrote for creating a plot and a thumbnail: [...]]]></description>
			<content:encoded><![CDATA[<p>This should have been a trivial task, but it turned out quite difficult. So these are my notes for the next time. Octave 4.2.2 under Linux Mint 19, using qt5ct plugin with GNU plot (or else I get <a href="https://billauer.se/blog/2019/12/octave-linux-plots/" target="_blank">blank plots</a>).</p>
<p>So this is the small function I wrote for creating a plot and a thumbnail:</p>
<pre>function []=toimg(fname, alt)

grid on;

saveas(gcf, sprintf('%s.png', fname), 'png');
print(gcf, sprintf('%s_thumb.png', fname), '-dpng', '-color', '-S280,210');

disp(sprintf('&lt;a href="/media/%s.png" target="_blank"&gt;&lt;img alt="%s" src="/media/%s_thumb.png" style="width: 280px; height: 210px;"&gt;&lt;/a&gt;', fname, alt, fname));</pre>
<p>The @alt argument becomes the image&#8217;s alternative text when shown on the web page.</p>
<p>The call to saveas() creates a 1200x900 image, and the print() call creates a 280x210 one (as specified directly). I take it that print() will create a 1200x900 without any specific argument for the size, but I left both methods, since this is how I ended up after struggling, and it&#8217;s better to have both possibilities shown.</p>
<p>To add some extra annoyment, toimg() always plots the current figure, which is typically the last figure plotted. Which is not necessarily the figure that has focus. As a matter of fact, even if the current figure is closed by clicking the upper-right X, it remains the current figure. Calling toimg() will make it reappear and get plotted. Which is really weird behavior.</p>
<p>The apparently only way around this is to use figure() to select the desired current figure before calling ioimg(), e.g.</p>
<pre>&gt;&gt; figure(4);</pre>
<p>The good news is that the figure numbers match those appearing on the windows&#8217; titles. This also explains why the numbering doesn&#8217;t reset when closing all figure windows manually. To really clear all figures, go</p>
<pre>&gt;&gt; close all hidden</pre>
<h3>Other oddities</h3>
<ul>
<li>ginput() simply doesn&#8217;t work. The workaround is to double-click any point (with left button) and the coordinates of this point are copied into the clipboard. Paste it anywhere. Odd, but not all that bad.</li>
<li>Zooming in with right-click and then left-click doesn&#8217;t affect axis(). As a result, saving the plot as an image is not affected by this zoom feature. Wonky workaround: Use the double-click trick above to obtain the coordinates of relevant corners, and use axis() to set them properly. Bonus: One gets the chance to adjust the figures for a sleek plot. If anyone knows how to save a plot as it&#8217;s shown by zooming, please comment below.</li>
</ul>
<p>&nbsp;</p>
]]></content:encoded>
			<wfw:commentRss>https://billauer.se/blog/2021/08/octave-export-plot-to-png/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
