From 0394a2a52af4d9a2e5ea4cc2afb332e2a59df60e Mon Sep 17 00:00:00 2001 From: Pascal Garber Date: Thu, 3 Sep 2026 22:55:41 +0200 Subject: [PATCH 1/6] Translations: restore lost markup and code literals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Seven catalogs had dropped an inline span or rewritten a code literal that the tutorial tells the reader to look up in the editor. Restored from the source string, leaving the surrounding translation as it was: - de: type lost its emphasis - fr: AND became "ET logique" — the opcode, not the conjunction - he: B lost its markup in the reset-flags line - ja: spinWheels dropped, X/$01 merged into X=$01, and the four spans of "Then why 6502?" replaced by quotes - uk: the second spinWheels became a pronoun - vi: JSR loop and BNE decrement were translated, so the instructions no longer assemble - zh_Hans: hex ranges gained en dashes, $05ff/$00/$0f lost their spans, and one sentence about the 16 colours went missing Claude-Session: https://claude.ai/code/session_01URLYj1wgGxVNRbBpNT8xAV --- packages/translations/de.po | 2 +- packages/translations/fr.po | 2 +- packages/translations/he.po | 2 +- packages/translations/ja.po | 12 ++++++------ packages/translations/uk.po | 2 +- packages/translations/vi.po | 4 ++-- packages/translations/zh_Hans.po | 10 +++++----- 7 files changed, 17 insertions(+), 17 deletions(-) diff --git a/packages/translations/de.po b/packages/translations/de.po index 21f4313e..52bd99d5 100644 --- a/packages/translations/de.po +++ b/packages/translations/de.po @@ -926,7 +926,7 @@ msgstr "In der Assemblersprache verwendet man normalerweise Labels mit Verzweigu #. TRANSLATORS: MDX-derived text from packages/learn/tutorial.mdx msgid "In the editor type (don't paste) the following code:" -msgstr "Tippe (nicht einfügen) den folgenden Code in den Editor:" +msgstr "Tippe (nicht einfügen) den folgenden Code in den Editor:" #. TRANSLATORS: MDX-derived text from packages/learn/tutorial.mdx msgid "In this assembler, we can define descriptive constants (or symbols) that represent numbers. The rest of the code can then simply use the constants instead of the literal number, which immediately makes it obvious what we're dealing with. You can use letters, digits and underscores in a name." diff --git a/packages/translations/fr.po b/packages/translations/fr.po index 74d531ba..35444798 100644 --- a/packages/translations/fr.po +++ b/packages/translations/fr.po @@ -1626,7 +1626,7 @@ msgstr "La sous-routine suivante, checkCollision, délègue à chec #. TRANSLATORS: MDX-derived text from packages/learn/tutorial.mdx msgid "The next subroutine, generateApplePosition, sets the apple location to a random position on the display. First, it loads a random byte into the accumulator ($fe is a random number generator in this simulator). This is stored into $00. Next, a different random byte is loaded into the accumulator, which is then AND-ed with the value $03. This part requires a bit of a detour." -msgstr "La sous-routine suivante, generateApplePosition, définit l'emplacement de la pomme à une position aléatoire sur l'affichage. D'abord, elle charge un octet aléatoire dans l'accumulateur ($fe est un générateur de nombres aléatoires dans ce simulateur). Celle-ci est stockée dans $00. Ensuite, un octet aléatoire différent est chargé dans l'accumulateur, qui est ensuite ET logique avec la valeur $03. Cette partie nécessite un petit détour." +msgstr "La sous-routine suivante, generateApplePosition, définit l'emplacement de la pomme à une position aléatoire sur l'affichage. D'abord, elle charge un octet aléatoire dans l'accumulateur ($fe est un générateur de nombres aléatoires dans ce simulateur). Celle-ci est stockée dans $00. Ensuite, un octet aléatoire différent est chargé dans l'accumulateur, qui subit ensuite un AND avec la valeur $03. Cette partie nécessite un petit détour." #. TRANSLATORS: MDX-derived text from packages/learn/tutorial.mdx msgid "The opposite of ADC is SBC (subtract with carry). Write a program that uses this instruction." diff --git a/packages/translations/he.po b/packages/translations/he.po index 10ae9cf1..10cd6518 100644 --- a/packages/translations/he.po +++ b/packages/translations/he.po @@ -1118,7 +1118,7 @@ msgstr "בסדר" #. TRANSLATORS: MDX-derived text from packages/learn/quick-help.mdx msgid "On reset (simulator): P = $30 (bit5 and B set; D and I clear)" -msgstr "לאחר איפוס (סימולטור): P = $30 (ביט 5 ו־B מוגדרים; D ו־I ריקים)" +msgstr "לאחר איפוס (סימולטור): P = $30 (ביט 5 ו־B מוגדרים; D ו־I ריקים)" #. TRANSLATORS: MDX-derived text from packages/learn/tutorial.mdx msgid "Once the values have been shifted down the snake, we have to work out what to do with the head. The direction is first loaded into A. LSR means \"logical shift right\", or \"shift all the bits one position to the right\". The least significant bit is shifted into the carry flag, so if the accumulator is 1, after LSR it is 0, with the carry flag set." diff --git a/packages/translations/ja.po b/packages/translations/ja.po index 10a4cbc9..c43f5261 100644 --- a/packages/translations/ja.po +++ b/packages/translations/ja.po @@ -496,7 +496,7 @@ msgstr "これらの横帯の中で動く限りは簡単です。右へは下位 #. TRANSLATORS: MDX-derived text from packages/learn/tutorial.mdx msgid "As stated before, the four directions are represented internally by the numbers 1, 2, 4 and 8. Each of these numbers is a power of 2, thus they are represented by a binary number with a single 1:" -msgstr "前述のとおり、4つの方向は内部的には 1, 2, 4, 8 で表されます。いずれも 2 の冪なので、ビットが1つだけ立った2進数になります:" +msgstr "前述のとおり、4つの方向は内部的には 1, 2, 4, 8 で表されます。いずれも 2 の冪なので、1 がひとつだけ立った2進数になります:" #. TRANSLATORS: Main action button label and menu item to assemble current code msgid "Assemble" @@ -959,7 +959,7 @@ msgstr "間接アドレッシングは絶対アドレスを使って別のアド #. TRANSLATORS: MDX-derived text from packages/learn/tutorial.mdx msgid "Indirect indexed is like indexed indirect but less insane. Instead of adding the X register to the address before dereferencing, the zero page address is dereferenced, and the Y register is added to the resulting address." -msgstr "間接インデックスはインデックス間接に似ていますが、参照の手順が逆です。参照X を足す代わりに、ゼロページアドレスを参照して得たアドレスに Y を足します。" +msgstr "間接インデックスはインデックス間接に似ていますが、参照の手順が逆です。参照X を足す代わりに、ゼロページアドレスを参照して得たアドレスに Y を足します。" #. TRANSLATORS: MDX-derived text from packages/learn/tutorial.mdx msgid "Indirect indexed: ($c0),Y" @@ -1073,7 +1073,7 @@ msgstr "メモリマップ" #. TRANSLATORS: MDX-derived text from packages/learn/tutorial.mdx msgid "Memory locations $01 and $02 contain the values $05 and $07 respectively. Think of ($00,X) as ($00 + X). In this case X is $01, so this simplifies to ($01). From here things proceed like standard indirect addressing - the two bytes at $01 and $02 ($05 and $07) are looked up to form the address $0705. This is the address that the Y register was stored into in the previous instruction, so the A register gets the same value as Y, albeit through a much more circuitous route. You won't see this much." -msgstr "メモリ $01$02 にはそれぞれ $05$07 が入っています。($00,X)($00 + X) と考えられ、ここでは X=$01 なので ($01) になります。以降は通常の間接参照と同様で、$01$02 の2バイト($05$07)から $0705 を形成します。これは直前の命令で Y を格納したアドレスなので、回り道ながら AY と同じ値になります。実際にはあまり見かけません。" +msgstr "メモリ $01$02 にはそれぞれ $05$07 が入っています。($00,X)($00 + X) と考えられ、ここでは X$01 なので ($01) になります。以降は通常の間接参照と同様で、$01$02 の2バイト($05$07)から $0705 を形成します。これは直前の命令で Y を格納したアドレスなので、回り道ながら AY と同じ値になります。実際にはあまり見かけません。" #. TRANSLATORS: Section heading label for console messages block msgid "Messages" @@ -1604,11 +1604,11 @@ msgstr "最後のセクションはプロセッサフラグです。各フラグ #. TRANSLATORS: MDX-derived text from packages/learn/tutorial.mdx msgid "The last subroutine, spinWheels, is just there because the game would run too fast otherwise. All spinWheels does is count X down from zero until it hits zero again. The first dex wraps, making X #$ff." -msgstr "最後の spinWheels はゲームが速すぎないようにするための遅延です。X を 0 から減らし 0 に戻るまで数えます。最初の dex はラップして X#$ff になります。" +msgstr "最後の spinWheels はゲームが速すぎないようにするための遅延です。spinWheelsX を 0 から減らし 0 に戻るまで数えるだけです。最初の dex はラップして X#$ff になります。" #. TRANSLATORS: MDX-derived text from packages/learn/tutorial.mdx msgid "The length is initialized to 4, so X starts off as 3. LDA $10,x loads the value of $13 into A, then STA $12,x stores this value into $15. X is decremented, and we loop. Now X is 2, so we load $12 and store it into $14. This loops while X is positive (BPL means \"branch if positive\")." -msgstr "長さは 4 なので X3 から開始します。LDA $10,x$13A に、STA $12,x でそれを $15 に保存。X をデクリメントしてループします。次に X=2 なので $12 を読み $14 に保存します。X が正の間(BPL)ループします。" +msgstr "長さは 4 なので X3 から開始します。LDA $10,x$13A に、STA $12,x でそれを $15 に保存。X をデクリメントしてループします。次に X2 なので $12 を読み $14 に保存します。X が正の間(BPL)ループします。" #. TRANSLATORS: MDX-derived text from packages/learn/tutorial.mdx msgid "The next bit updates the head of the snake depending on the direction. This is probably the most complicated part of the code, and it's all reliant on how memory locations map to the screen, so let's look at that in more detail." @@ -1664,7 +1664,7 @@ msgstr "ゼロページには、ゲーム状態の変数がいくつか格納さ #. TRANSLATORS: MDX-derived text from packages/learn/tutorial.mdx msgid "Then why 6502? Why not a useful assembly language, like x86? Well, I don't think learning x86 is useful. I don't think you'll ever have to write assembly language in your day job - for some people this is an academic exercise, something to expand your mind and your thinking. 6502 was originally written in a different age, a time when the majority of developers were writing assembly directly, rather than in these new-fangled high-level programming languages. So, it was designed to be written by humans. More modern assembly languages are meant to written by compilers, so let's leave it to them. Plus, 6502 is fun. Nobody ever called x86 fun." -msgstr "ではなぜ 6502 なのか?x86 のような「実用的」なアセンブリでは?私は x86 を学ぶ必要性は高くないと思います。仕事でアセンブリを書く機会はほぼありません。これは思考を広げる学術的な練習でもあります。6502 は多くの開発者が高級言語ではなく直接アセンブリを書いていた時代に、人間が書く前提で設計されました。現代のアセンブリはコンパイラが出力するためのものです。しかも 6502 は「楽しい」。x86 を「楽しい」と言う人はいません。" +msgstr "ではなぜ 6502 なのか?x86 のような実用的なアセンブリでは?私は x86 を学ぶ必要性は高くないと思います。仕事でアセンブリを書く機会はほぼありません。これは思考を広げる学術的な練習でもあります。6502 は多くの開発者が高級言語ではなく直接アセンブリを書いていた時代に、人間が書く前提で設計されました。現代のアセンブリはコンパイラが出力するためのものです。しかも 6502 は楽しい。x86 を楽しいと言う人はいません。" #. TRANSLATORS: MDX-derived text from packages/learn/tutorial.mdx msgid "There's also a very practical reason: retro game development. The homebrew scene is alive and well—new games are still released for the NES (some even as physical cartridges) and for many other classic systems; the NES is just an example. Even if your target is something like the SNES, starting with 6502 pays off: the SNES CPU (65C816) is backward-compatible with the 6502, so the fundamentals you learn here—registers, addressing modes, and the core instruction set—carry straight over." diff --git a/packages/translations/uk.po b/packages/translations/uk.po index 49bbba5d..93f8cf6d 100644 --- a/packages/translations/uk.po +++ b/packages/translations/uk.po @@ -1604,7 +1604,7 @@ msgstr "Останній розділ показує прапорці проце #. TRANSLATORS: MDX-derived text from packages/learn/tutorial.mdx msgid "The last subroutine, spinWheels, is just there because the game would run too fast otherwise. All spinWheels does is count X down from zero until it hits zero again. The first dex wraps, making X #$ff." -msgstr "Остання підпрограма, spinWheels, потрібна, бо інакше гра працюватиме занадто швидко. Вона просто зменшує X від нуля, доки той знову не стане нулем. Перший dex спричиняє «обгортання», роблячи X рівним #$ff." +msgstr "Остання підпрограма, spinWheels, потрібна, бо інакше гра працюватиме занадто швидко. spinWheels просто зменшує X від нуля, доки той знову не стане нулем. Перший dex спричиняє «обгортання», роблячи X рівним #$ff." #. TRANSLATORS: MDX-derived text from packages/learn/tutorial.mdx msgid "The length is initialized to 4, so X starts off as 3. LDA $10,x loads the value of $13 into A, then STA $12,x stores this value into $15. X is decremented, and we loop. Now X is 2, so we load $12 and store it into $14. This loops while X is positive (BPL means \"branch if positive\")." diff --git a/packages/translations/vi.po b/packages/translations/vi.po index 428ae466..f1229fc5 100644 --- a/packages/translations/vi.po +++ b/packages/translations/vi.po @@ -1573,7 +1573,7 @@ msgstr "ác dụng của việc này là che giấu hai bit có ý nghĩa nhỏ #. TRANSLATORS: MDX-derived text from packages/learn/tutorial.mdx msgid "The first instruction causes execution to jump to the init label. This sets X, then returns to the next instruction, JSR loop. This jumps to the loop label, which increments X until it is equal to $05. After that we return to the next instruction, JSR end, which jumps to the end of the file. This illustrates how JSR and RTS can be used together to create modular code." -msgstr "Lệnh đầu tiên khiến việc thực thi nhảy tới nhãn init. Lệnh này đặt X, sau đó quay lại lệnh tiếp theo, vòng lặp JSR. Điều này chuyển tới nhãn vòng lặp, nhãn này tăng dần X cho đến khi bằng $05. Sau đó, chúng ta quay lại lệnh tiếp theo, JSR end, lệnh này sẽ nhảy xuống cuối tệp. Điều này minh họa cách JSRRTS có thể được sử dụng cùng nhau để tạo mã mô-đun." +msgstr "Lệnh đầu tiên khiến việc thực thi nhảy tới nhãn init. Lệnh này đặt X, sau đó quay lại lệnh tiếp theo, JSR loop. Điều này chuyển tới nhãn loop, nhãn này tăng dần X cho đến khi bằng $05. Sau đó, chúng ta quay lại lệnh tiếp theo, JSR end, lệnh này sẽ nhảy xuống cuối tệp. Điều này minh họa cách JSRRTS có thể được sử dụng cùng nhau để tạo mã mô-đun." #. TRANSLATORS: MDX-derived text from packages/learn/tutorial.mdx msgid "The first line shows the A, X and Y registers (A is often called the \"accumulator\"). Each register holds a single byte. Most operations work on the contents of these registers." @@ -1617,7 +1617,7 @@ msgstr "Bit tiếp theo cập nhật phần đầu của con rắn tùy theo hư #. TRANSLATORS: MDX-derived text from packages/learn/tutorial.mdx msgid "The next line, BNE decrement, will shift execution to the decrement label if the Z flag is set to 0 (meaning that the two values in the CPX comparison were not equal), otherwise it does nothing and we store X to $0201, then finish the program." -msgstr "Dòng tiếp theo, Giảm BNE, sẽ chuyển việc thực thi sang nhãn giảm nếu cờ Z được đặt thành 0 (có nghĩa là hai giá trị trong so sánh CPX không bằng nhau), nếu không thì không làm gì cả và chúng tôi lưu trữ X thành $0201, sau đó kết thúc chương trình." +msgstr "Dòng tiếp theo, BNE decrement, sẽ chuyển việc thực thi sang nhãn decrement nếu cờ Z được đặt thành 0 (có nghĩa là hai giá trị trong so sánh CPX không bằng nhau), nếu không thì không làm gì cả và chúng tôi lưu trữ X thành $0201, sau đó kết thúc chương trình." #. TRANSLATORS: MDX-derived text from packages/learn/tutorial.mdx msgid "The next subroutine, checkCollision, defers to checkAppleCollision and checkSnakeCollision. checkAppleCollision just checks to see if the two bytes holding the location of the apple match the two bytes holding the location of the head. If they do, the length is increased and a new apple position is generated." diff --git a/packages/translations/zh_Hans.po b/packages/translations/zh_Hans.po index eec03ab5..d5ad4d8b 100644 --- a/packages/translations/zh_Hans.po +++ b/packages/translations/zh_Hans.po @@ -576,7 +576,7 @@ msgstr "进位标志(C):加法产生进位时置位;减法需要借位 #. TRANSLATORS: MDX-derived text from packages/learn/tutorial.mdx msgid "Change one of the pixels to draw at the bottom-right corner (memory location $05ff)." -msgstr "试着修改内存中最后一个像素的颜色 (内存地址 $05ff)。" +msgstr "试着修改内存中最后一个像素的颜色 (内存地址 $05ff)。" #. TRANSLATORS: ViewSwitcher/tab title for the Editor view msgid "Code" @@ -770,7 +770,7 @@ msgstr "第一行:$0200-$021F,第二行:$0220-< #. TRANSLATORS: MDX-derived text from packages/learn/tutorial.mdx msgid "First we load the value $08 into the X register. The next line is a label. Labels just mark certain points in a program so we can return to them later. After the label we decrement X, store it to $0200 (the top-left pixel), and then compare it to the value $03. CPX compares the value in the X register with another value. If the two values are equal, the Z flag is set to 1, otherwise it is set to 0." -msgstr "首先将值 $08 加载到寄存器 X。下一行是一个标签。标签用于标记程序中的位置,以便稍后返回。经过标签后对 X 递减,将其存入 $0200(左上角像素),然后与 $03 比较。CPX 比较 X 寄存器与另一个值;若相等则置位 Z,否则清零。" +msgstr "首先将值 $08 加载到寄存器 X。下一行是一个标签。标签用于标记程序中的位置,以便稍后返回。经过标签后对 X 递减,将其存入 $0200(左上角像素),然后与 $03 比较。CPX 比较 X 寄存器与另一个值;若两值相等,则 Z 标志置为 1,否则置为 0。" #. TRANSLATORS: MDX-derived text from packages/learn/tutorial.mdx msgid "First, readkeys checks to see if one of the direction keys (W, A, S, D) was pressed, and if so, sets the direction of the snake accordingly. Then, checkCollision checks to see if the snake collided with itself or the apple. updateSnake updates the internal representation of the snake, based on its direction. Next, the apple and snake are drawn. Finally, spinWheels makes the processor do some busy work, to stop the game from running too quickly. Think of it like a sleep command. The game keeps running until the snake collides with the wall or itself." @@ -1188,7 +1188,7 @@ msgstr "预处理…" #. TRANSLATORS: MDX-derived text from packages/learn/tutorial.mdx msgid "Press Step again to execute the second instruction. The top-left pixel of the game console should now be white. This simulator uses the memory locations $0200 to $05ff to draw pixels on its display. The values $00 to $0f represent 16 different colours ($00 is black and $01 is white), so storing the value $01 at memory location $0200 draws a white pixel at the top left corner. This is simpler than how an actual computer would output video, but it'll do for now." -msgstr "再次点击单步执行第二条指令。第一个像素变为白色。显示器使用内存地址$0200$05ff存储像素值。$00表示黑色,$01表示白色。STA $0200将寄存器A的值($01)存储到内存地址$0200,绘制一个白色像素。" +msgstr "再次点击单步执行第二条指令。游戏机的左上角像素现在应为白色。此模拟器使用内存地址 $0200$05ff 在显示器上绘制像素。$00$0f 表示 16 种不同的颜色($00 为黑色,$01 为白色),因此将值 $01 存入内存地址 $0200,就会在左上角绘制一个白色像素。这比真实计算机输出视频的方式简单,但目前够用了。" #. TRANSLATORS: Label for next button in share dialog msgid "Preview" @@ -1605,7 +1605,7 @@ msgstr "最后一部分展示处理器标志。每个标志占一位,因此七 #. TRANSLATORS: MDX-derived text from packages/learn/tutorial.mdx msgid "The last subroutine, spinWheels, is just there because the game would run too fast otherwise. All spinWheels does is count X down from zero until it hits zero again. The first dex wraps, making X #$ff." -msgstr "最后一个子程序 spinWheels 用于避免游戏运行过快。它仅将 X 从零向下计数,直到再次回到零。第一次 dex 会回绕,使 X 变为 #$ff。" +msgstr "最后一个子程序 spinWheels 用于避免游戏运行过快。spinWheels 仅将 X 从零向下计数,直到再次回到零。第一次 dex 会回绕,使 X 变为 #$ff。" #. TRANSLATORS: MDX-derived text from packages/learn/tutorial.mdx msgid "The length is initialized to 4, so X starts off as 3. LDA $10,x loads the value of $13 into A, then STA $12,x stores this value into $15. X is decremented, and we loop. Now X is 2, so we load $12 and store it into $14. This loops while X is positive (BPL means \"branch if positive\")." @@ -1808,7 +1808,7 @@ msgstr "黄色" #. TRANSLATORS: MDX-derived text from packages/learn/tutorial.mdx msgid "You can think of the screen as four horizontal strips of 32 × 8 pixels. These strips map to $0200-$02ff, $0300-$03ff, $0400-$04ff and $0500-$05ff. The first rows of pixels are $0200-$021f, $0220-$023f, $0240-$025f, etc." -msgstr "可以将屏幕看作 4 个横向条,每个条 32 × 8 像素。它们映射到 $0200–$02ff$0300–$03ff$0400–$04ff$0500–$05ff。第一行像素是 $0200–$021f$0220–$023f$0240–$025f 等。" +msgstr "可以将屏幕看作 4 个横向条,每个条 32 × 8 像素。它们映射到 $0200-$02ff$0300-$03ff$0400-$04ff$0500-$05ff。第一行像素是 $0200-$021f$0220-$023f$0240-$025f 等。" #. TRANSLATORS: MDX-derived text from packages/learn/tutorial.mdx msgid "You've seen TAX. You can probably guess what TAY, TXA and TYA do, but write some code to test your assumptions." From 02bb227febca4ff650f39a07c76963e8958af7ad Mon Sep 17 00:00:00 2001 From: Pascal Garber Date: Thu, 3 Sep 2026 22:55:56 +0200 Subject: [PATCH 2/6] Translations: repair Tamil markup and terminology MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Tamil catalog arrived from Weblate at 456/456 translated, which is what msgfmt measures. It also carried six msgstr that Pango cannot parse — the only catalog with any — and ten translated code identifiers. Markup: a URL that swallowed half a sentence, an with no closing >, an unclosed that truncated the BVS/BVC list, "< tt>" with spaces so it rendered as literal text, and two paragraphs that lost their closing tag. These feed a GtkLabel with use-markup, where a parse error blanks the label. Code: checkSnakeCollision, illegalMove, generateApplePosition, loop and JSR end were translated, and "0001 AND 0001" turned the AND opcode into the Tamil word for "and". Restored verbatim. Terminology: "assembly" appeared as four different wrong words — பேரவை and சட்டசபை (a legislative assembly), சபை, கூட்டி (an adder) — alongside the correct அசெம்பிளி, including in "Open Assembly File" and the application name itself. Also fixed "to" as an e-mail recipient, "lead" as the metal, "current" as electric current, "logic" as a lawsuit, and "van der" as a delivery van in a surname. Four entries were left half-English by the machine pass and are now translated. The submission's house style is kept throughout: no grantha letters, s/j/z written with ச, as in the rest of the file. Tamil is listed as "Needs validation" — @TamilNeram, or any Tamil speaker, please check the wording. Claude-Session: https://claude.ai/code/session_01URLYj1wgGxVNRbBpNT8xAV --- packages/translations/LINGUAS | 2 +- packages/translations/README.md | 19 +++++++++++ packages/translations/ta.po | 58 ++++++++++++++++----------------- 3 files changed, 49 insertions(+), 30 deletions(-) diff --git a/packages/translations/LINGUAS b/packages/translations/LINGUAS index 73d74f55..99418baf 100644 --- a/packages/translations/LINGUAS +++ b/packages/translations/LINGUAS @@ -10,7 +10,7 @@ nl pl pt pt_BR +ta uk vi zh_Hans -ta diff --git a/packages/translations/README.md b/packages/translations/README.md index ad26d912..d0588c3b 100644 --- a/packages/translations/README.md +++ b/packages/translations/README.md @@ -44,6 +44,24 @@ Support for every new language is welcome. 3. **External links**: - You may replace links to English pages with equivalent high-quality pages in the target language. - Prefer reputable sources (e.g., Wikipedia) when a high-quality article exists in the target language. + - In-document anchors like `href="#jumping"` navigate inside the tutorial — keep the fragment as it is. + +4. **Inline `` is code**, not prose: subroutine names, labels, opcodes, registers and addresses + the reader looks up in the editor. Copy it through unchanged. A translated `illegalMove` + describes code the reader cannot find, and a translated `JSR end` no longer assembles. + `` may hold either — translate a UI button label like `Step`, matching how that button + is translated elsewhere in the catalog, and leave code like `BVS/BVC` alone. + +### Checking your work + +```bash +gjsify workspace @learn6502/translations check +``` + +`msgfmt` only asks whether a `msgstr` is non-empty, so a catalog can report 100% translated while +shipping markup that Pango refuses to parse — which blanks the whole paragraph in the GNOME app — +or a code identifier that was translated. The `check` script catches both, and CI runs it on every +pull request. It says nothing about language quality; that still needs a native speaker. ## Resources @@ -71,6 +89,7 @@ Below is an overview of all languages, their sources, and human validation statu | Finnish | fi | Contributed by Jiri Grönroos | Jiri Grönroos | Validated | | Hebrew | he | Contributed by Menachem (@naattxx) | @naattxx | Validated | | Indonesian | id | Contributed by Arif Budiman (@arifpedia) | @arifpedia | Validated | +| Tamil | ta | Contributed by தமிழ் நேரம் (@TamilNeram) via Weblate | — | Needs validation | To volunteer as a validator, please use [Weblate](https://hosted.weblate.org/projects/eu-jumplink-learn6502/app/) or report your feedback in the [issue tracker](https://github.com/JumpLink/Learn6502/issues). diff --git a/packages/translations/ta.po b/packages/translations/ta.po index 48e84c7b..3128fdd5 100644 --- a/packages/translations/ta.po +++ b/packages/translations/ta.po @@ -19,7 +19,7 @@ msgstr "" #. TRANSLATORS: MDX-derived text from packages/learn/tutorial.mdx msgid "(Actually, I've been reliably informed that 6502 processors are still being produced by Western Design Center and sold to hobbyists, so clearly 6502 isn't a dead language! Who knew?)" -msgstr "(உண்மையில், 6502 செயலிகள் இன்னும் மேற்கத்திய வடிவமைப்பு மையம் மற்றும் இல்லை என்பது யாருக்குத் தெரியும்?)" +msgstr "(உண்மையில், 6502 செயலிகளை Western Design Center இன்னும் தயாரித்து வருகிறது என்றும், அவை பொழுதுபோக்கு ஆர்வலர்களுக்கு விற்கப்படுகின்றன என்றும் எனக்கு நம்பத்தகுந்த தகவல் கிடைத்துள்ளது. எனவே 6502 ஒரு செத்த மொழி அல்ல என்பது தெளிவு! யாருக்குத் தெரியும்?)" msgid "6502" msgstr "6502" @@ -38,7 +38,7 @@ msgstr "6502 அசெம்பிளி மொழியில் பல கி #. TRANSLATORS: MDX-derived text from packages/learn/tutorial.mdx msgid "Willem van der Jagt made a fully annotated gist of this source code, so follow along with that for more details." -msgstr "Willem வேன் der Jagt இந்த மூலக் குறியீட்டின் முழு விவரக்குறிப்புச் சுருக்கத்தை உருவாக்கினார், மேலும் விவரங்களுக்கு அதைப் பின்பற்றவும்." +msgstr "Willem van der Jagt இந்த மூலக் குறியீட்டிற்கு முழுமையாக விளக்கமளிக்கப்பட்ட ஒரு gist உருவாக்கியுள்ளார்; மேலும் விவரங்களுக்கு அதைப் பின்தொடரவும்." #. TRANSLATORS: MDX-derived text from packages/learn/quick-help.mdx msgid "$00-$01: Apple position" @@ -326,11 +326,11 @@ msgstr "a9 மற்றும் c9 ஆகியவை மு #. TRANSLATORS: MDX-derived text from packages/learn/tutorial.mdx msgid "checkSnakeCollision loops through the snake's body segments, checking each byte pair against the head pair. If there is a match, then game over." -msgstr "செக் பாம்பு மோதல் பாம்பின் உடல் பகுதிகள் வழியாகச் சென்று, ஒவ்வொரு பைட் சோடியையும் தலை சோடிக்கு எதிராகச் சரிபார்க்கிறது. போட்டி இருந்தால், ஆட்டம் முடிந்தது." +msgstr "checkSnakeCollision பாம்பின் உடல் பகுதிகள் வழியாகச் சுழன்று, ஒவ்வொரு பைட் சோடியையும் தலைச் சோடியுடன் ஒப்பிடுகிறது. பொருத்தம் இருந்தால், ஆட்டம் முடிந்தது." #. TRANSLATORS: MDX-derived text from packages/learn/tutorial.mdx msgid "init and loop are both subroutines. init initializes the game state, and loop is the main game loop." -msgstr "init மற்றும் லூப் இரண்டும் சப்ரூட்டின்கள். init கேம் நிலையை துவக்குகிறது, மேலும் லூப் என்பது முக்கிய கேம் லூப் ஆகும்." +msgstr "init மற்றும் loop இரண்டும் சப்ரூட்டின்கள். init ஆட்ட நிலையைத் துவக்குகிறது, loop என்பது முதன்மையான ஆட்ட வளையம் ஆகும்." #. TRANSLATORS: Instructions for completing submission on GitHub, including info about large examples msgid "" @@ -348,7 +348,7 @@ msgstr "6502 அறிவுறுத்தல் தொகுப்பின் #. TRANSLATORS: About dialog menu item label msgid "About Learn 6502 Assembly" -msgstr "பற்றி அறிய 6502 பேரவை" +msgstr "6502 அசெம்பிளியைக் கற்றுக்கொள் பற்றி" #. TRANSLATORS: MDX-derived text from packages/learn/tutorial.mdx msgid "Absolute,X and absolute,Y: $c000,X and $c000,Y" @@ -464,7 +464,7 @@ msgstr "கிட்டத்தட்ட முடிந்தது!" #. TRANSLATORS: MDX-derived text from packages/learn/quick-help.mdx msgid "Also useful: BIT, CMP/CPX/CPY, PHP/PLP, TSX/TXS, SED/CLD, SEI/CLI, BMI/BPL, BVS/BVC" -msgstr "மேலும் பயனுள்ளதாக இருக்கும்: BIT, CMP/CPX/CPY, PHP/PLP, TSX/TXS, SED/CLD, SEI/CLI, BMI/BPL, BVC/" +msgstr "மேலும் பயனுள்ளவை: BIT, CMP/CPX/CPY, PHP/PLP, TSX/TXS, SED/CLD, SEI/CLI, BMI/BPL, BVS/BVC" #. TRANSLATORS: MDX-derived text from packages/learn/tutorial.mdx msgid "An important thing to notice here is the distinction between ADC #$01 and ADC $01. The first one adds the value $01 to the A register, but the second adds the value stored at memory location $01 to the A register." @@ -528,11 +528,11 @@ msgid "Assembling code ..." msgstr "குறியீட்டை அசெம்பிள் செய்கிறது..." msgid "Assembly" -msgstr "கூட்டி" +msgstr "அசெம்பிளி" #. TRANSLATORS: File filter name for assembly source files msgid "Assembly Files" -msgstr "பேரவை கோப்புகள்" +msgstr "அசெம்பிளி கோப்புகள்" #. TRANSLATORS: MDX-derived text from packages/learn/tutorial.mdx msgid "At a low level, this subroutine is slightly more complex. First, the length is loaded into the X register, which is then decremented. The snippet below shows the starting memory for the snake." @@ -762,7 +762,7 @@ msgstr "இறுதியாக, பைட் $03 என்பது #. TRANSLATORS: Release notes for version 0.1.0 msgid "First release of Learn 6502 Assembly for GNOME" -msgstr "க்னோமிற்கான Learn 6502 சட்டசபையின் முதல் வெளியீடு" +msgstr "க்னோமிற்கான 6502 அசெம்பிளியைக் கற்றுக்கொள் இன் முதல் வெளியீடு" #. TRANSLATORS: MDX-derived text from packages/learn/quick-help.mdx msgid "First row: $0200-$021F, second row: $0220-$023F, etc." @@ -819,11 +819,11 @@ msgstr "Games now automatically இடைநிறுத்தம் when switch #. TRANSLATORS: Note about validation status - keep it concise and encouraging msgid "German and Spanish have been validated by contributors. Other languages are AI‑assisted drafts and need human validation." -msgstr "செர்மன் and ச்பானிச் have been validated by contributors. மற்றொன்று மொழிகள் அரே AI‑assisted drafts and need human validation." +msgstr "செர்மன், ச்பானிச் மொழிகள் பங்களிப்பாளர்களால் சரிபார்க்கப்பட்டுள்ளன. மற்ற மொழிகள் AI உதவியுடன் உருவாக்கப்பட்ட வரைவுகள்; அவற்றுக்கு மனிதர்களின் சரிபார்ப்பு தேவை." #. TRANSLATORS: MDX-derived text from packages/learn/tutorial.mdx msgid "Going between sections is more complicated, as we have to take into account the most significant byte as well. For example, going down from $02e1 should lead to $0301. Luckily, this is fairly easy to accomplish. Adding $20 to $e1 results in $01 and sets the carry bit. If the carry bit was set, we know we also need to increment the most significant byte." -msgstr "Going between பிரிவுகள் is more complicated, அச் we have பெறுநர் take into account the பெரும்பாலானவை significant byte அச் well. க்கு example, going down இருந்து $02e1 should ஈயம் பெறுநர் $0301. Luckily, this is fairly easy பெறுநர் accomplish. Adding $20 பெறுநர் $e1 முடிவுகள் in $01 and sets the carry bit. If the carry துணுக்கு was set, we know we also need பெறுநர் increment the பெரும்பாலானவை significant byte." +msgstr "பிரிவுகளுக்கு இடையே நகர்வது இன்னும் சிக்கலானது, ஏனெனில் உயர் வரிசைப் பைட்டையும் கணக்கில் கொள்ள வேண்டும். எடுத்துக்காட்டாக, $02e1 இலிருந்து கீழே செல்வது $0301 க்கு இட்டுச்செல்ல வேண்டும். நல்லவேளையாக, இதைச் செய்வது மிகவும் எளிது. $e1 உடன் $20 ஐச் சேர்த்தால் $01 கிடைக்கும், மேலும் கேரி பிட் அமைக்கப்படும். கேரி பிட் அமைக்கப்பட்டிருந்தால், உயர் வரிசைப் பைட்டையும் அதிகரிக்க வேண்டும் என்பது நமக்குத் தெரியும்." #. TRANSLATORS: Description for examples placeholder msgid "" @@ -835,7 +835,7 @@ msgstr "" #. TRANSLATORS: Release notes for version 0.6.0 - community contribution feature msgid "Got your own cool 6502 creation? Share it with the community! You can now submit your code examples directly from within the app. Just hit the share button, and we'll automatically create a GitHub issue and pull request for you." -msgstr "Got your own cool 6502 creation? பங்கு it with the community! You can now submit your குறியீடு examples directly இருந்து within the app. Just hit the பங்கு button, and we'll automatically create a GitHub சிக்கல் and pull request க்கு you." +msgstr "உங்களுடைய சொந்த 6502 படைப்பு ஏதேனும் உள்ளதா? அதைச் சமூகத்துடன் பகிருங்கள்! இப்போது உங்கள் குறியீட்டு எடுத்துக்காட்டுகளை நேரடியாகப் பயன்பாட்டிற்குள்ளிருந்தே சமர்ப்பிக்கலாம். பகிர் பொத்தானை அழுத்தினால் போதும் — உங்களுக்காக GitHub சிக்கலையும் இழுக்கக் கோரிக்கையையும் நாங்கள் தானாகவே உருவாக்கிவிடுவோம்." #. TRANSLATORS: Tooltip for accent color option Green #. TRANSLATORS: Tooltip for primary color option Green @@ -917,7 +917,7 @@ msgstr "மென்மையான விளையாட்டுக்கா #. TRANSLATORS: MDX-derived text from packages/learn/tutorial.mdx msgid "In assembly language, you'll usually use labels with branch instructions. When assembled though, this label is converted to a single-byte relative offset (a number of bytes to go backwards or forwards from the next instruction) so branch instructions can only go forward and back around 256 bytes. This means they can only be used to move around local code. For moving further you'll need to use the jumping instructions." -msgstr "பேரவை மொழியில், நீங்கள் வழக்கமாக கிளை வழிமுறைகளுடன் லேபிள்களைப் பயன்படுத்துவீர்கள். இருப்பினும், அசெம்பிள் செய்யும் போது, இந்த சிட்டை ஒற்றை-பைட் ரிலேடிவ் ஆஃப்செட்டாக மாற்றப்படுகிறது (அடுத்த அறிவுறுத்தலில் இருந்து பின்னோக்கி அல்லது முன்னோக்கிச் செல்ல பல பைட்டுகள்) எனவே கிளை அறிவுறுத்தல்கள் 256 பைட்டுகள் மட்டுமே முன்னோக்கிச் செல்ல முடியும். அதாவது, உள்ளக குறியீட்டை நகர்த்துவதற்கு மட்டுமே அவற்றைப் பயன்படுத்த முடியும். மேலும் செல்ல, நீங்கள் சம்பிங் வழிமுறைகளைப் பயன்படுத்த வேண்டும்." +msgstr "அசெம்பிளி மொழியில், நீங்கள் வழக்கமாகக் கிளை வழிமுறைகளுடன் லேபிள்களைப் பயன்படுத்துவீர்கள். இருப்பினும், அசெம்பிள் செய்யும் போது, இந்த லேபிள் ஒற்றை-பைட் ரிலேடிவ் ஆஃப்செட்டாக மாற்றப்படுகிறது (அடுத்த வழிமுறையிலிருந்து பின்னோக்கி அல்லது முன்னோக்கிச் செல்ல வேண்டிய பைட்டுகளின் எண்ணிக்கை). எனவே கிளை வழிமுறைகள் சுமார் 256 பைட்டுகள் மட்டுமே முன்னோக்கியும் பின்னோக்கியும் செல்ல முடியும். அதாவது, அவற்றை உள்ளக குறியீட்டினுள் நகர்வதற்கு மட்டுமே பயன்படுத்த முடியும். அதற்கு மேல் செல்ல, நீங்கள் தாவல் வழிமுறைகளைப் பயன்படுத்த வேண்டும்." #. TRANSLATORS: MDX-derived text from packages/learn/tutorial.mdx msgid "In the editor type (don't paste) the following code:" @@ -937,7 +937,7 @@ msgstr "இந்த எடுத்துக்காட்டில், $f #. TRANSLATORS: MDX-derived text from packages/learn/tutorial.mdx msgid "In this tiny tutorial I'm going to show you how to get started writing 6502 assembly language. The 6502 processor was massive in the seventies and eighties, powering famous computers like the BBC Micro, Atari 2600, Commodore 64, Apple II, and the Nintendo Entertainment System. Bender in Futurama has a 6502 processor for a brain. Even the Terminator was programmed in 6502." -msgstr "இந்த சிறிய டுடோரியலில், 6502 அசெம்பிளி மொழியை எவ்வாறு எழுதுவது என்பதை நான் உங்களுக்குக் காண்பிக்கப் போகிறேன். 6502 செயலி எழுபதுகள் மற்றும் எண்பதுகளில் மிகப்பெரியதாக இருந்தது, BBC மைக்ரோ, Atari 2600, 2600, 26> போன்ற பிரபலமான கணினிகளை இயக்கும். href=\"https://en.wikipedia.org/wiki/Commodore_64\">கொமடோர் 64, ஆப்பிள் II, மற்றும் . Futurama இல் பெண்டர் ஒரு மூளைக்கான 6502 செயலி உள்ளது. டெர்மினேட்டர் கூட 6502 இல் திட்டமிடப்பட்டது." +msgstr "இந்த சிறிய டுடோரியலில், 6502 அசெம்பிளி மொழியை எழுதத் தொடங்குவது எப்படி என்பதை உங்களுக்குக் காண்பிக்கப் போகிறேன். 6502 செயலி எழுபதுகளிலும் எண்பதுகளிலும் மிகவும் பரவலாக இருந்தது; BBC Micro, Atari 2600, Commodore 64, Apple II, Nintendo Entertainment System போன்ற புகழ்பெற்ற கணினிகளை இயக்கியது. Futurama இல் வரும் Bender இன் மூளையாக ஒரு 6502 செயலி உள்ளது. Terminator கூட 6502 இல் நிரலாக்கப்பட்டிருந்தது." #. TRANSLATORS: Register name and description in debugger msgid "Index register X: Used for addressing" @@ -960,7 +960,7 @@ msgstr "மறைமுக முகவரியானது மற்றொர #. TRANSLATORS: MDX-derived text from packages/learn/tutorial.mdx msgid "Indirect indexed is like indexed indirect but less insane. Instead of adding the X register to the address before dereferencing, the zero page address is dereferenced, and the Y register is added to the resulting address." -msgstr "மறைமுக குறியீடானது குறியிடப்பட்ட மறைமுகம் போன்றது ஆனால் குறைவான பைத்தியம். < tt>X பதிவை முன் dereferencing என்ற முகவரியில் சேர்ப்பதற்குப் பதிலாக, பூச்சியப் பக்கத்தின் முகவரி dereferences செய்யப்பட்டு, அதன் விளைவாக வரும் முகவரியில் Y பதிவேடு சேர்க்கப்படும்." +msgstr "மறைமுக அட்டவணையிடல் என்பது அட்டவணையிடப்பட்ட மறைமுகத்தைப் போன்றது, ஆனால் அவ்வளவு சிக்கலானது அல்ல. மறைமுகமாக அணுகுவதற்கு முன் முகவரியுடன் X பதிவைச் சேர்ப்பதற்குப் பதிலாக, பூச்சியப் பக்க முகவரி முதலில் அணுகப்படுகிறது, பின்னர் அதன் விளைவாக வரும் முகவரியுடன் Y பதிவு சேர்க்கப்படுகிறது." #. TRANSLATORS: MDX-derived text from packages/learn/tutorial.mdx msgid "Indirect indexed: ($c0),Y" @@ -1033,7 +1033,7 @@ msgstr "கற்றுக்கொள்ளுங்கள்" #. TRANSLATORS: Application title shown in the header bar msgid "Learn 6502 Assembly" -msgstr "6502 சட்டசபையை அறிக" +msgstr "6502 அசெம்பிளியைக் கற்றுக்கொள்" #. TRANSLATORS: Release notes for version 0.6.5 - GNOME 50 runtime update msgid "Learn 6502 now runs on GNOME 50! Thanks to Sabri Ünal for contributing the runtime update." @@ -1126,7 +1126,7 @@ msgstr "மதிப்புகள் பாம்புக்கு கீழ #. TRANSLATORS: File dialog title when choosing an assembly source file msgid "Open Assembly File" -msgstr "பேரவை கோப்பைத் திறக்கவும்" +msgstr "அசெம்பிளி கோப்பைத் திறக்கவும்" #. TRANSLATORS: Menu item to open a file msgid "Open..." @@ -1358,7 +1358,7 @@ msgstr "சேமி" #. TRANSLATORS: File dialog title for saving a new assembly file msgid "Save Assembly File" -msgstr "பேரவை கோப்பை சேமிக்கவும்" +msgstr "அசெம்பிளி கோப்பைச் சேமிக்கவும்" #. TRANSLATORS: Menu item to save to a new file (Save As) msgid "Save as..." @@ -1387,7 +1387,7 @@ msgstr "வரவேற்கிறோம்! இந்தோனேசிய ம #. TRANSLATORS: MDX-derived text from packages/learn/tutorial.mdx msgid "Seriously though, I think it's valuable to have an understanding of assembly language. Assembly language is the lowest level of abstraction in computers - the point at which the code is still readable. Assembly language translates directly to the bytes that are executed by your computer's processor. If you understand how it works, you've basically become a computer magician." -msgstr "தீவிரமாக இருந்தாலும், பேரவை மொழியைப் புரிந்துகொள்வது மதிப்புமிக்கது என்று நான் நினைக்கிறேன். அசெம்பிளி மொழி என்பது கணினிகளில் சுருக்கத்தின் மிகக் குறைந்த நிலை - குறியீடு இன்னும் படிக்கக்கூடிய புள்ளியாகும். உங்கள் கணினியின் செயலி மூலம் செயல்படுத்தப்படும் பைட்டுகளுக்கு பேரவை மொழி நேரடியாக மொழிபெயர்க்கப்படுகிறது. இது எவ்வாறு செயல்படுகிறது என்பதை நீங்கள் புரிந்து கொண்டால், நீங்கள் அடிப்படையில் கணினி வித்தைக்காரர் ஆகிவிடுவீர்கள்." +msgstr "நகைச்சுவை ஒருபுறம் இருக்க, அசெம்பிளி மொழியைப் புரிந்துகொள்வது மதிப்புமிக்கது என்று நான் நினைக்கிறேன். அசெம்பிளி மொழி என்பது கணினிகளில் சுருக்கத்தின் மிகக் கீழ் நிலை — குறியீடு இன்னும் படிக்கக்கூடியதாக இருக்கும் புள்ளி. அசெம்பிளி மொழி, உங்கள் கணினியின் செயலி இயக்கும் பைட்டுகளுக்கு நேரடியாக மொழிபெயர்க்கப்படுகிறது. இது எவ்வாறு செயல்படுகிறது என்பதை நீங்கள் புரிந்துகொண்டால், நீங்கள் அடிப்படையில் ஒரு கணினி வித்தைக்காரர் ஆகிவிடுவீர்கள்." #. TRANSLATORS: Release notes for version 0.7.0 - Hebrew translation. Keep the greeting 'Shalom!', the thanks 'Toda raba!' and the name 'Menachem (naattxx)' as they are. msgid "Shalom! We are happy to welcome Hebrew translations, fully contributed by Menachem (naattxx). Toda raba!" @@ -1444,7 +1444,7 @@ msgstr "எனவே, உள்ளே நுழைவோம்! இந்தப #. TRANSLATORS: MDX-derived text from packages/learn/tutorial.mdx msgid "So, looking at upKey, if the current direction is down (4), the bit test will be zero. BNE means \"branch if the zero flag is clear\", so in this case we'll branch to illegalMove, which just returns from the subroutine. Otherwise, the new direction (1 in this case) is stored in the appropriate memory location." -msgstr "So, looking at upKey, if the மின்னோட்ட்ம், ஓட்டம் direction is down (4), the bit தேர்வு will be zero. BNE என்பது \"பூச்சியக் கொடி தெளிவாக இருந்தால் கிளை\" என்று பொருள்படும், எனவே இந்த விசயத்தில் நாம் சப்ரூட்டினிலிருந்து திரும்பும் சட்டவிரோத நகர்வு க்கு பிரிவோம். Otherwise, the புதிய direction (1 in this case) is stored in the appropriate memory location." +msgstr "எனவே upKey ஐப் பாருங்கள்: தற்போதைய திசை கீழ்நோக்கி (4) இருந்தால், பிட் சோதனை பூச்சியமாக இருக்கும். BNE என்பது \"பூச்சியக் கொடி அமைக்கப்படவில்லை என்றால் கிளை பிரி\" என்று பொருள்படும். எனவே இந்த விசயத்தில் நாம் illegalMove க்குக் கிளை பிரிவோம்; அது சப்ரூட்டினிலிருந்து வெறுமனே திரும்பிவிடும். இல்லையெனில், புதிய திசை (இங்கே 1) உரிய நினைவக இடத்தில் சேமிக்கப்படும்." #. TRANSLATORS: MDX-derived text from packages/learn/tutorial.mdx msgid "So, the instruction STA $0200 stores the value of the A register to memory location $0200. Click Step four more times to execute the rest of the instructions, keeping an eye on the A register as it changes." @@ -1491,7 +1491,7 @@ msgstr "படி" #. TRANSLATORS: Description for tutorial section msgid "Step-by-step guide to 6502 assembly" -msgstr "6502 சட்டசபைக்கு படிப்படியான வழிகாட்டி" +msgstr "6502 அசெம்பிளிக்கான படிப்படியான வழிகாட்டி" #. TRANSLATORS: Status line shown when simulator execution stops msgid "Stopped" @@ -1529,19 +1529,19 @@ msgstr "6502 ஆனது 16-பிட் முகவரி பச்சைப #. TRANSLATORS: MDX-derived text from packages/learn/tutorial.mdx msgid "The BIT opcode is similar to AND, but the calculation is only used to set the zero flag - the actual result is discarded. The zero flag is set only if the result of AND-ing the accumulator with argument is zero. When we're looking at powers of two, the zero flag will only be set if the two numbers are not the same. For example, 0001 AND 0001 is not zero, but 0001 AND 0010 is zero." -msgstr "BIT ஆப்கோட் AND ஐப் போன்றது, ஆனால் கணக்கீடு பூச்சியக் கொடியை அமைக்க மட்டுமே பயன்படுத்தப்படுகிறது - உண்மையான முடிவு நிராகரிக்கப்பட்டது. வாதத்துடன் கூடிய AND-ing இன் முடிவு பூச்சியமாக இருந்தால் மட்டுமே பூச்சியக் கொடி அமைக்கப்படும். நாம் இரண்டின் சக்திகளைப் பார்க்கும்போது, இரண்டு எண்களும் ஒரே மாதிரியாக இல்லாவிட்டால் மட்டுமே பூச்சியக் கொடி அமைக்கப்படும். எடுத்துக்காட்டாக, 0001 மற்றும் 0001 என்பது பூச்சியம் அல்ல, ஆனால் 0001 மற்றும் 0010 என்பது பூச்சியம்." +msgstr "BIT ஆப்கோட் AND ஐப் போன்றது, ஆனால் கணக்கீடு பூச்சியக் கொடியை அமைக்க மட்டுமே பயன்படுகிறது — உண்மையான முடிவு நிராகரிக்கப்படுகிறது. திரட்டியை வாதத்துடன் AND செய்த முடிவு பூச்சியமாக இருந்தால் மட்டுமே பூச்சியக் கொடி அமைக்கப்படும். இரண்டின் அடுக்குகளைப் பார்க்கும்போது, இரண்டு எண்களும் ஒன்றாக இல்லாவிட்டால் மட்டுமே பூச்சியக் கொடி அமைக்கப்படும். எடுத்துக்காட்டாக, 0001 AND 0001 பூச்சியம் அல்ல, ஆனால் 0001 AND 0010 பூச்சியம்." #. TRANSLATORS: MDX-derived text from packages/learn/tutorial.mdx msgid "The init subroutine defers to two subroutines, initSnake and generateApplePosition. initSnake sets the snake direction, length, and then loads the initial memory locations of the snake head and body. The byte pair at $10 contains the screen location of the head, the pair at $12 contains the location of the single body segment, and $14 contains the location of the tail (the tail is the last segment of the body and is drawn in black to keep the snake moving). This happens in the following code:" -msgstr "init சப்ரூட்டீன் initSnake மற்றும் GenerateApplePosition ஆகிய இரண்டு சப்ரூட்டின்களை ஒத்திவைக்கிறது. initSnake பாம்பின் திசை, நீளம் ஆகியவற்றை அமைத்து, பின்னர் பாம்பின் தலை மற்றும் உடலின் ஆரம்ப நினைவக இடங்களை ஏற்றுகிறது. $10 இல் உள்ள பைட் இணை தலையின் திரை இருப்பிடத்தைக் கொண்டுள்ளது, $12 இல் உள்ள இணை ஒற்றை உடல் பிரிவின் இருப்பிடத்தைக் கொண்டுள்ளது, $14 வால் இருப்பிடத்தைக் கொண்டுள்ளது (வால் உடலின் கடைசிப் பகுதி மற்றும் பாம்பை நகர்த்துவதற்காக கருப்பு நிறத்தில் வரையப்பட்டுள்ளது). இது பின்வரும் குறியீட்டில் நிகழ்கிறது:" +msgstr "init சப்ரூட்டின், initSnake மற்றும் generateApplePosition ஆகிய இரண்டு சப்ரூட்டின்களிடம் வேலையை ஒப்படைக்கிறது. initSnake பாம்பின் திசையையும் நீளத்தையும் அமைத்து, பின்னர் பாம்பின் தலை மற்றும் உடலின் தொடக்க நினைவக இடங்களை ஏற்றுகிறது. $10 இல் உள்ள பைட் சோடி தலையின் திரை இருப்பிடத்தைக் கொண்டுள்ளது, $12 இல் உள்ள சோடி ஒற்றை உடல் பகுதியின் இருப்பிடத்தைக் கொண்டுள்ளது, $14 வாலின் இருப்பிடத்தைக் கொண்டுள்ளது (வால் என்பது உடலின் கடைசிப் பகுதி; பாம்பு நகர்ந்துகொண்டே இருப்பதற்காக அது கருப்பு நிறத்தில் வரையப்படுகிறது). இது பின்வரும் குறியீட்டில் நிகழ்கிறது:" #. TRANSLATORS: MDX-derived text from packages/learn/tutorial.mdx msgid "The loop subroutine itself just calls a number of subroutines sequentially, before looping back on itself:" -msgstr "லூப் சப்ரூட்டீன் தன்னைத்தானே மீண்டும் வளையச்செய்யும் முன், தொடர்ச்சியாக பல சப்ரூட்டின்களை அழைக்கிறது:" +msgstr "loop சப்ரூட்டின், தன்னைத்தானே மீண்டும் அழைப்பதற்கு முன், தொடர்ச்சியாகப் பல சப்ரூட்டின்களை அழைக்கிறது:" #. TRANSLATORS: Detailed description of Accumulator register msgid "The Accumulator (A) is the primary register for arithmetic and logic operations. All basic calculations like ADD, SUBTRACT, AND, OR, etc. use this register. It's the most frequently used register in 6502 assembly programming." -msgstr "அக்யூமுலேட்டர் (A) என்பது எண்கணிதம் மற்றும் வழக்கு செயல்பாடுகளுக்கான முதன்மைப் பதிவேடாகும். ADD, SUBTRACT, AND, OR, போன்ற அனைத்து அடிப்படைக் கணக்கீடுகளும் இந்தப் பதிவேட்டைப் பயன்படுத்துகின்றன. இது 6502 பேரவை நிரலாக்கத்தில் அடிக்கடி பயன்படுத்தப்படும் பதிவு." +msgstr "திரட்டி (A) என்பது எண்கணித மற்றும் தருக்கச் செயல்பாடுகளுக்கான முதன்மைப் பதிவு. ADD, SUBTRACT, AND, OR போன்ற அனைத்து அடிப்படைக் கணக்கீடுகளும் இந்தப் பதிவைப் பயன்படுத்துகின்றன. 6502 அசெம்பிளி நிரலாக்கத்தில் மிக அடிக்கடி பயன்படுத்தப்படும் பதிவு இதுவே." #. TRANSLATORS: Detailed description of Program Counter register msgid "The Program Counter contains the memory address of the next instruction to execute. It automatically increments after each instruction and can be modified by jumps, branches, calls, and returns. Essential for program flow control and debugging." @@ -1573,7 +1573,7 @@ msgstr "இதன் விளைவு, திரட்டியின் ம #. TRANSLATORS: MDX-derived text from packages/learn/tutorial.mdx msgid "The first instruction causes execution to jump to the init label. This sets X, then returns to the next instruction, JSR loop. This jumps to the loop label, which increments X until it is equal to $05. After that we return to the next instruction, JSR end, which jumps to the end of the file. This illustrates how JSR and RTS can be used together to create modular code." -msgstr "முதல் அறிவுறுத்தலானது இயக்கத்தை init லேபிளுக்கு மாற்றுகிறது. இது Xஐ அமைக்கிறது, பின்னர் அடுத்த அறிவுறுத்தலான JSR loopக்குத் திரும்புகிறது. இது லூப் லேபிளுக்குத் தாவுகிறது, இது $05க்கு சமமாக இருக்கும் வரை X அதிகரிக்கிறது. அதன் பிறகு நாம் அடுத்த அறிவுறுத்தலுக்குத் திரும்புவோம், JSR முடிவு, இது கோப்பின் இறுதிக்குத் தாவுகிறது. மட்டு குறியீட்டை உருவாக்க JSR மற்றும் RTS எவ்வாறு ஒன்றாகப் பயன்படுத்தப்படலாம் என்பதை இது விளக்குகிறது." +msgstr "முதல் வழிமுறை, இயக்கத்தை init லேபிளுக்குத் தாவச் செய்கிறது. இது X ஐ அமைத்துவிட்டு, அடுத்த வழிமுறையான JSR loop க்குத் திரும்புகிறது. இது loop லேபிளுக்குத் தாவுகிறது; அது X$05 க்குச் சமமாகும் வரை அதிகரிக்கிறது. அதன் பிறகு நாம் அடுத்த வழிமுறையான JSR end க்குத் திரும்புகிறோம்; அது கோப்பின் இறுதிக்குத் தாவுகிறது. தொகுதி வாரியான குறியீட்டை உருவாக்க JSR மற்றும் RTS ஆகியவற்றை எவ்வாறு சேர்த்துப் பயன்படுத்தலாம் என்பதை இது விளக்குகிறது." #. TRANSLATORS: MDX-derived text from packages/learn/tutorial.mdx msgid "The first line shows the A, X and Y registers (A is often called the \"accumulator\"). Each register holds a single byte. Most operations work on the contents of these registers." @@ -1617,7 +1617,7 @@ msgstr "அடுத்த பிட் திசையைப் பொறுத #. TRANSLATORS: MDX-derived text from packages/learn/tutorial.mdx msgid "The next line, BNE decrement, will shift execution to the decrement label if the Z flag is set to 0 (meaning that the two values in the CPX comparison were not equal), otherwise it does nothing and we store X to $0201, then finish the program." -msgstr "அடுத்த வரி, BNE decrement, Z கொடியை 0 என அமைத்தால், செயல்பாட்டினை குறைப்பு லேபிளுக்கு மாற்றும் (அதாவது CPX ஒப்பீட்டில் உள்ள இரண்டு மதிப்புகளும் சமமாக இல்லை), இல்லையெனில் அது ஒன்றும் செய்யாது, நாங்கள் X ஐச் சேமித்து, பின்னர் $0." +msgstr "அடுத்த வரி, BNE decrement, Z கொடி 0 ஆக அமைக்கப்பட்டிருந்தால் (அதாவது CPX ஒப்பீட்டில் உள்ள இரண்டு மதிப்புகளும் சமமாக இல்லை என்றால்) இயக்கத்தை decrement லேபிளுக்கு மாற்றும்; இல்லையெனில் அது ஒன்றும் செய்யாது, நாம் X$0201 இல் சேமித்துவிட்டு நிரலை முடிக்கிறோம்." #. TRANSLATORS: MDX-derived text from packages/learn/tutorial.mdx msgid "The next subroutine, checkCollision, defers to checkAppleCollision and checkSnakeCollision. checkAppleCollision just checks to see if the two bytes holding the location of the apple match the two bytes holding the location of the head. If they do, the length is increased and a new apple position is generated." @@ -1625,7 +1625,7 @@ msgstr "அடுத்த சப்ரூட்டீன், checkCollision #. TRANSLATORS: MDX-derived text from packages/learn/tutorial.mdx msgid "The next subroutine, generateApplePosition, sets the apple location to a random position on the display. First, it loads a random byte into the accumulator ($fe is a random number generator in this simulator). This is stored into $00. Next, a different random byte is loaded into the accumulator, which is then AND-ed with the value $03. This part requires a bit of a detour." -msgstr "அடுத்த சப்ரூட்டீன், GenerateApplePosition, ஆப்பிளின் இருப்பிடத்தை காட்சியில் சீரற்ற நிலைக்கு அமைக்கிறது. முதலில், இது ஒரு சீரற்ற பைட்டை திரட்டியில் ஏற்றுகிறது ($fe என்பது இந்த சிமுலேட்டரில் உள்ள ஒரு சீரற்ற எண் செனரேட்டர்). இது $00 இல் சேமிக்கப்படுகிறது. அடுத்து, வேறு ஒரு சீரற்ற பைட் திரட்டியில் ஏற்றப்படும், அது $03 மதிப்புடன் AND-ed. இந்த பகுதிக்கு சற்று மாற்றுப்பாதை தேவைப்படுகிறது." +msgstr "அடுத்த சப்ரூட்டின், generateApplePosition, ஆப்பிளின் இருப்பிடத்தைக் காட்சியில் ஒரு சீரற்ற நிலைக்கு அமைக்கிறது. முதலில், இது ஒரு சீரற்ற பைட்டைத் திரட்டியில் ஏற்றுகிறது ($fe என்பது இந்த சிமுலேட்டரில் உள்ள சீரற்ற எண் உருவாக்கி). இது $00 இல் சேமிக்கப்படுகிறது. அடுத்து, வேறொரு சீரற்ற பைட் திரட்டியில் ஏற்றப்பட்டு, அது $03 மதிப்புடன் AND செய்யப்படுகிறது. இந்தப் பகுதிக்குச் சற்று மாற்றுப்பாதை தேவைப்படுகிறது." #. TRANSLATORS: MDX-derived text from packages/learn/tutorial.mdx msgid "The opposite of ADC is SBC (subtract with carry). Write a program that uses this instruction." @@ -1665,7 +1665,7 @@ msgstr "நினைவகத்தின் பூச்சியப் பக #. TRANSLATORS: MDX-derived text from packages/learn/tutorial.mdx msgid "Then why 6502? Why not a useful assembly language, like x86? Well, I don't think learning x86 is useful. I don't think you'll ever have to write assembly language in your day job - for some people this is an academic exercise, something to expand your mind and your thinking. 6502 was originally written in a different age, a time when the majority of developers were writing assembly directly, rather than in these new-fangled high-level programming languages. So, it was designed to be written by humans. More modern assembly languages are meant to written by compilers, so let's leave it to them. Plus, 6502 is fun. Nobody ever called x86 fun." -msgstr "பிறகு ஏன் 6502? x86 போன்ற பயனுள்ள பேரவை மொழி ஏன் இல்லை? சரி, நான் x86 கற்றல் பயனுள்ளதாக இல்லை என்று நினைக்கிறேன். உங்கள் நாள் வேலையில் நீங்கள் பேரவை மொழியை எப்பொழுதும் எழுத வேண்டும் என்று நான் நினைக்கவில்லை - சிலருக்கு இது ஒரு கல்விப் பயிற்சி, உங்கள் மனதையும் உங்கள் சிந்தனையையும் விரிவுபடுத்தும் ஒன்று. 6502 முதலில் வேறு ஒரு வயதில் எழுதப்பட்டது, பெரும்பாலான உருவாக்குபவர்கள் இந்த புதிய-விசித்திரமான உயர்-நிலை நிரலாக்க மொழிகளில் அல்லாமல், நேரடியாக அசெம்பிளி எழுதும் காலகட்டம். எனவே, இது மனிதர்களால் எழுத வடிவமைக்கப்பட்டுள்ளது. நவீன அசெம்பிளி மொழிகள் தொகுப்பாளர்களால் எழுதப்பட வேண்டும், எனவே அதை அவர்களிடமே விட்டுவிடுவோம். கூடுதலாக, 6502 வேடிக்கை. யாரும் x86 ஐ fun என்று அழைக்கவில்லை." +msgstr "பிறகு ஏன் 6502? x86 போன்ற பயனுள்ள அசெம்பிளி மொழி ஏன் இல்லை? சரி, x86 கற்பது பயனுள்ளது என்று நான் நினைக்கவில்லை. உங்கள் அன்றாட வேலையில் நீங்கள் எப்போதாவது அசெம்பிளி மொழியை எழுத வேண்டியிருக்கும் என்றும் நான் நினைக்கவில்லை — சிலருக்கு இது ஒரு கல்விப் பயிற்சி, மனதையும் சிந்தனையையும் விரிவுபடுத்தும் ஒன்று. 6502 வேறொரு காலகட்டத்தில் உருவாக்கப்பட்டது; அப்போது பெரும்பாலான உருவாக்குநர்கள் இந்தப் புதிய உயர்நிலை நிரலாக்க மொழிகளில் அல்லாமல், நேரடியாக அசெம்பிளி எழுதிக்கொண்டிருந்தார்கள். எனவே இது மனிதர்கள் எழுதுவதற்கென வடிவமைக்கப்பட்டது. நவீன அசெம்பிளி மொழிகள் தொகுப்பிகள் எழுதுவதற்கானவை; எனவே அதை அவற்றிடமே விட்டுவிடுவோம். மேலும், 6502 வேடிக்கையானது. x86 ஐ வேடிக்கையானது என்று யாரும் சொன்னதில்லை." #. TRANSLATORS: MDX-derived text from packages/learn/tutorial.mdx msgid "There's also a very practical reason: retro game development. The homebrew scene is alive and well—new games are still released for the NES (some even as physical cartridges) and for many other classic systems; the NES is just an example. Even if your target is something like the SNES, starting with 6502 pays off: the SNES CPU (65C816) is backward-compatible with the 6502, so the fundamentals you learn here—registers, addressing modes, and the core instruction set—carry straight over." From b415bb7435968292884408f2d17f770be2678c2d Mon Sep 17 00:00:00 2001 From: Pascal Garber Date: Thu, 3 Sep 2026 22:56:05 +0200 Subject: [PATCH 3/6] Translations: gate catalog structure in CI msgfmt only asks whether a msgstr is non-empty, so a catalog reports 100% translated while shipping markup Pango refuses to parse or a code identifier that was translated. Both classes reached main and neither is visible in the Weblate percentage. check.js reads every catalog and enforces four rules: the markup parses, no tag the source opens is dropped, content survives byte-for-byte, and format placeholders survive. Tag comparison is by name, so a translator can still point an at a target-language article as the README allows; only in-document anchors are pinned. One literal is exempt, with its reason written down: "label" in the addressing-mode list is a placeholder word. Calibrated against all sixteen catalogs: it finds the seventeen real defects fixed in the previous two commits and nothing else, and reverting any one of them makes it red again. The catalog directory is the working directory, not the script's own. Under `--app gjs` the script runs as a bundle in dist/, where a script-relative path finds no .po files at all: every rule then passes over an empty set and the run reports success having read nothing. Checking LINGUAS makes that state loud instead, since it is the one failure this check must never report as success. Claude-Session: https://claude.ai/code/session_01URLYj1wgGxVNRbBpNT8xAV --- .github/workflows/ci.yml | 7 + packages/translations/check.js | 232 +++++++++++++++++++++++++++++ packages/translations/package.json | 3 + 3 files changed, 242 insertions(+) create mode 100644 packages/translations/check.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 67f2ef82..395fc8f4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -85,6 +85,13 @@ jobs: - name: Install dependencies run: gjsify install --immutable + # Ahead of the type checks so a broken catalog is reported as itself. + # `msgfmt` only asks whether a `msgstr` is non-empty, so this is the only + # gate that sees markup Pango cannot parse or a translated code + # identifier — both of which have reached `main` before. + - name: Validate translation catalogs + run: gjsify workspace @learn6502/translations check + # Build the workspace deps that publish built output (`@learn6502/core`, # `examples`, `learn` ship `dist/*.d.ts`; `common-ui` exports source). # Without this, type-checking common-ui/app-* fails with TS2307 "Cannot diff --git a/packages/translations/check.js b/packages/translations/check.js new file mode 100644 index 00000000..ab527396 --- /dev/null +++ b/packages/translations/check.js @@ -0,0 +1,232 @@ +/** + * Structural validation for the translation catalogs. + * + * `msgfmt` only asks whether a `msgstr` is non-empty, so a catalog can report + * "100% translated" while shipping markup that Pango refuses to parse and code + * identifiers the tutorial tells the reader to look for in the editor. Both + * classes reached `main` unnoticed — see the rules below for the concrete + * damage each one guards against. + * + * Run via `gjsify workspace @learn6502/translations check`. + */ + +import { readFileSync, readdirSync } from "node:fs"; + +// The package directory, which is where `gjsify workspace` runs a script from. +// Deliberately not derived from `import.meta.url`: the `--app gjs` target runs +// a bundle written to `dist/`, so a script-relative path finds no catalogs at +// all there and the run reports success having read nothing. +const PO_DIR = "."; + +/** + * Inline `` spans whose content is prose rather than code, and may + * therefore be translated. Every entry needs a reason: the default is that a + * `` span is a literal the reader retypes or searches for, so exempting one + * is a claim that it is not. + */ +const PROSE_TT = new Set([ + // Metasyntactic placeholder in the addressing-mode list: "Relative + // (label): Branch uses signed 8-bit offset ...". It stands for "any + // label", not for a label named `label`. + "label", +]); + +/** Reads every `.po` next to this script. */ +function readCatalogs() { + return readdirSync(PO_DIR) + .filter((name) => name.endsWith(".po")) + .sort() + .map((name) => ({ lang: name.slice(0, -3), entries: parsePo(`${PO_DIR}/${name}`) })); +} + +/** + * Minimal PO reader: enough for `msgid`/`msgstr` pairs with continuation lines. + * The catalogs are generated with `noWrap`, but Weblate still folds long + * strings, so continuations have to be joined. + */ +function parsePo(path) { + const entries = []; + let msgid = null; + let msgstr = null; + let field = null; + + const flush = () => { + if (msgid) entries.push({ msgid, msgstr: msgstr ?? "" }); + msgid = null; + msgstr = null; + field = null; + }; + + for (const raw of readFileSync(path, "utf8").split("\n")) { + const line = raw.trim(); + if (line.startsWith("msgid ")) { + if (field === "msgstr") flush(); + msgid = unquote(line.slice(6)); + field = "msgid"; + } else if (line.startsWith("msgstr ")) { + msgstr = unquote(line.slice(7)); + field = "msgstr"; + } else if (line.startsWith('"') && field) { + if (field === "msgid") msgid += unquote(line); + else msgstr += unquote(line); + } else if (line === "" && field === "msgstr") { + flush(); + } else if (line.startsWith("#")) { + // comment or flag line — carries no data this check needs + } + } + flush(); + return entries.filter((entry) => entry.msgid !== ""); +} + +function unquote(value) { + const trimmed = value.trim(); + const inner = trimmed.startsWith('"') && trimmed.endsWith('"') ? trimmed.slice(1, -1) : trimmed; + return inner.replace(/\\"/g, '"').replace(/\\n/g, "\n").replace(/\\t/g, "\t").replace(/\\\\/g, "\\"); +} + +const TAG = /<\/?([a-zA-Z][a-zA-Z0-9]*)(?:\s[^>]*)?>/g; +const TT = /([\s\S]*?)<\/tt>/g; +const PLACEHOLDER = /%[sd]|%\d+\$[sd]|\{\w+\}|%\{\w+\}/g; + +/** + * Rule 1 — the markup must parse. + * + * Tutorial paragraphs are rendered through `GtkLabel use-markup` (see + * `packages/learn/tsx/components/gtk/`). Pango treats a malformed fragment as + * an error and leaves the label blank, so a broken `` silently + * costs the reader a whole paragraph. + */ +function markupErrors(text) { + const stack = []; + const errors = []; + + // Reject the shapes a text parser would otherwise accept: a `<` that opens + // no valid tag, and an unquoted or unterminated attribute list. + for (const match of text.matchAll(/<[^>]*(?:>|$)/g)) { + const token = match[0]; + if (!token.endsWith(">")) errors.push(`unterminated tag: ${clip(token)}`); + else if (!/^<\/?[a-zA-Z][a-zA-Z0-9]*(?:\s+[a-zA-Z-]+="[^"]*")*\s*\/?>$/.test(token)) + errors.push(`malformed tag: ${clip(token)}`); + } + + for (const match of text.matchAll(TAG)) { + const name = match[1]; + if (match[0].startsWith("`); + } else if (!match[0].endsWith("/>")) { + stack.push(name); + } + } + for (const name of stack) errors.push(`unclosed <${name}>`); + return errors; +} + +/** + * Rule 2 — every tag the source opens must survive translation. + * + * Compared by tag name as a multiset, not as a sequence of full tags: word + * order legitimately moves an emphasised span, and the README lets a translator + * point an `` at an equivalent page in the target language. What is not + * allowed is dropping the span, which costs the emphasis or the whole link. + * + * In-document anchors are the exception — `` navigates + * inside the tutorial, so a rewritten fragment is a dead link, not a + * localisation. + */ +function tagCounts(text) { + return count( + [...text.matchAll(TAG)].map((match) => { + const fragment = /]*href="(#[^"]*)"/.exec(match[0]); + return fragment ? `` : match[0].replace(/^<(\/?)([a-zA-Z0-9]+).*$/, "<$1$2>"); + }) + ); +} + +/** + * Rule 3 — `` content is code and must survive byte-for-byte. + * + * These spans name subroutines, labels, opcodes and addresses that the reader + * looks up in the editor. Translating `illegalMove` or + * `0001 AND 0001` produces a tutorial that describes code the reader + * cannot find, and `JSR end` no longer assembles. + */ +function codeLiterals(text) { + return count([...text.matchAll(TT)].map((match) => match[1]).filter((literal) => !PROSE_TT.has(literal))); +} + +/** Rule 4 — format placeholders must survive, or the string breaks at runtime. */ +function placeholders(text) { + return count([...text.matchAll(PLACEHOLDER)].map((match) => match[0])); +} + +function count(values) { + const counts = new Map(); + for (const value of values) counts.set(value, (counts.get(value) ?? 0) + 1); + return counts; +} + +/** Members of `expected` that `actual` does not cover, with multiplicity. */ +function missing(expected, actual) { + const lost = []; + for (const [value, times] of expected) { + const short = times - (actual.get(value) ?? 0); + for (let i = 0; i < short; i++) lost.push(value); + } + return lost; +} + +function clip(text) { + return text.length > 70 ? `${text.slice(0, 70)}…` : text; +} + +function check() { + const failures = []; + const catalogs = readCatalogs(); + + // A run that reads nothing passes every rule, which is the one failure this + // check must never report as success. LINGUAS is the count the build uses. + const expected = readFileSync(`${PO_DIR}/LINGUAS`, "utf8").split("\n").filter(Boolean).length; + if (catalogs.length !== expected) { + console.error(`Read ${catalogs.length} catalogs from ${PO_DIR}, but LINGUAS names ${expected}.`); + return 1; + } + + for (const { lang, entries } of catalogs) { + for (const { msgid, msgstr } of entries) { + if (msgstr === "") continue; + const problems = []; + + for (const error of markupErrors(msgstr)) problems.push(`invalid markup — ${error}`); + + const lostTags = missing(tagCounts(msgid), tagCounts(msgstr)); + if (lostTags.length) problems.push(`dropped tags — ${lostTags.map(clip).join(", ")}`); + + const lostCode = missing(codeLiterals(msgid), codeLiterals(msgstr)); + if (lostCode.length) + problems.push(`altered code literals — ${lostCode.map((l) => `${clip(l)}`).join(", ")}`); + + const lostPlaceholders = missing(placeholders(msgid), placeholders(msgstr)); + if (lostPlaceholders.length) problems.push(`dropped placeholders — ${lostPlaceholders.join(", ")}`); + + if (problems.length) failures.push({ lang, msgid, problems }); + } + } + + for (const { lang, msgid, problems } of failures) { + console.error(`${lang}.po: ${clip(msgid.replace(/\n/g, " "))}`); + for (const problem of problems) console.error(` ${problem}`); + } + + if (failures.length) { + console.error(`\n${failures.length} translation(s) failed structural validation.`); + return 1; + } + + console.log("All translations passed structural validation."); + return 0; +} + +const status = check(); +// `gjsify run` keeps the GJS main loop alive, so signal the result explicitly. +if (status !== 0) throw new Error("translation check failed"); diff --git a/packages/translations/package.json b/packages/translations/package.json index cd7ac952..14689a5d 100644 --- a/packages/translations/package.json +++ b/packages/translations/package.json @@ -14,6 +14,9 @@ "build:gen": "gjsify build build.js --app gjs --outfile dist/build.gen.js", "build:run": "gjsify run dist/build.gen.js", "build": "gjsify run build:gen && gjsify run build:run", + "check:gen": "gjsify build check.js --app gjs --outfile dist/check.gen.js", + "check:run": "gjsify run dist/check.gen.js", + "check": "gjsify run check:gen && gjsify run check:run", "clear": "rm -rf dist" }, "devDependencies": { From d300df2592371c9da79cd85638f77737870a03dd Mon Sep 17 00:00:00 2001 From: Pascal Garber Date: Fri, 4 Sep 2026 00:07:11 +0200 Subject: [PATCH 4/6] Translations: close gaps in the catalog checker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The check passed content GtkLabel renders as an empty string, which is the one thing it exists to prevent. Measured against Gtk.Label with use-markup on GTK 4 / Pango 1.57, it accepted every one of these: A & B unescaped ampersand — GMarkup entity error A B HTML-only entity name — unknown to GMarkup y tag Pango does not know, added alongside the source tags so the multiset comparison stayed happy x attribute the tag does not support LDA same, and the literal stops being compared because the rule matched a bare only here href dropped from a link Zeile %s %s placeholder added rather than dropped and rejected , which GtkLabel accepts. Rule 1 is now a model of the vocabulary GtkLabel takes — tag allowlist, per-tag attributes, required href, GMarkup entities — and rule 4 compares placeholders in both directions. One scan feeds rules 1 and 2 so a token the balance check sees and the tag comparison misses is no longer possible. The PO reader gained msgctxt and plural forms. It skipped msgstr[0] silently, so the day a plural is extracted, all four rules would have passed it without looking: the same failure as reading no catalogs at all, one shape smaller. The catalog guard compares names against LINGUAS instead of counting, so a stray .po beside a missing one no longer keeps the total right while leaving a shipped language unchecked. English source strings are checked once each, since a broken fragment in tutorial.mdx reaches every catalog through the .pot. A gate that has stopped biting reports success over content nobody has looked at, so 21 pairs the rules must classify correctly now run before any catalog is read. Verified by mutation: weakening the tag allowlist, the entity pattern, the attribute table, the comparison or the added-placeholder check each turns the run red on the self-test alone. No behaviour change on the current catalogs: 0 findings on this branch, the same 32 strings on main. Claude-Session: https://claude.ai/code/session_01URLYj1wgGxVNRbBpNT8xAV --- packages/translations/check.js | 381 ++++++++++++++++++++++++++------- 1 file changed, 300 insertions(+), 81 deletions(-) diff --git a/packages/translations/check.js b/packages/translations/check.js index ab527396..a632cbd2 100644 --- a/packages/translations/check.js +++ b/packages/translations/check.js @@ -7,6 +7,11 @@ * classes reached `main` unnoticed — see the rules below for the concrete * damage each one guards against. * + * Every markup shape this file accepts or rejects was measured against + * `Gtk.Label` with `use-markup` on GTK 4 / Pango 1.57, not inferred from the + * documentation: a label whose markup fails to parse renders as an empty + * string, which is the damage the check exists to prevent. + * * Run via `gjsify workspace @learn6502/translations check`. */ @@ -31,7 +36,50 @@ const PROSE_TT = new Set([ "label", ]); -/** Reads every `.po` next to this script. */ +/** + * Tags a label may contain. + * + * Pango also accepts `` and the `` root, and both are left out on + * purpose: `` carries typed attribute values (`` + * fails to parse because `x` is not a colour), and validating those is a whole + * second parser. Nothing in the source strings uses either — the tutorial's + * vocabulary is `a`, `b`, `i`, `sub` and `tt` — so a translation that grows one + * is a defect worth reporting even when Pango would have accepted it. + */ +const TAGS = new Set(["a", "b", "big", "i", "s", "small", "sub", "sup", "tt", "u"]); + +/** + * Attributes each tag accepts, and the ones it cannot do without. + * + * Pango rejects the whole label for an attribute it does not know — `` and `` both parse to nothing — and `` without + * `href` is rejected outright ("Attribute 'href' was missing on the tag"). + * Every tag outside this table takes no attributes at all. + */ +const TAG_ATTRIBUTES = { a: { allowed: new Set(["href", "title"]), required: ["href"] } }; + +/** + * The entities GMarkup resolves, plus numeric character references. + * + * Anything else — a bare `&`, an HTML-only name like ` `, a stray `&` in a + * retargeted link's query string — is a parse error, so the paragraph renders + * blank. This is the failure a translator is most likely to introduce by hand. + */ +const ENTITY = /^&(?:amp|lt|gt|quot|apos|#\d+|#x[0-9a-fA-F]+);/; + +/** One markup token: `<…>`, or a trailing `<` with no `>` after it. */ +const TOKEN = /<[^>]*(?:>|$)/g; +const OPEN_TAG = /^<([a-zA-Z][a-zA-Z0-9]*)((?:\s+[a-zA-Z_:][\w.:-]*\s*=\s*(?:"[^"]*"|'[^']*'))*)\s*(\/?)>$/; +const CLOSE_TAG = /^<\/([a-zA-Z][a-zA-Z0-9]*)\s*>$/; +const ATTRIBUTE = /\s+([a-zA-Z_:][\w.:-]*)\s*=\s*(?:"[^"]*"|'[^']*')/g; + +const TT = /([\s\S]*?)<\/tt>/g; +const PLACEHOLDER = /%[sd]|%\d+\$[sd]|\{\w+\}|%\{\w+\}/g; + +/** PO string escapes. Anything else after a backslash stands for itself. */ +const ESCAPES = { n: "\n", t: "\t", r: "\r", '"': '"', "\\": "\\" }; + +/** Reads every `.po` in the catalog directory. */ function readCatalogs() { return readdirSync(PO_DIR) .filter((name) => name.endsWith(".po")) @@ -40,86 +88,150 @@ function readCatalogs() { } /** - * Minimal PO reader: enough for `msgid`/`msgstr` pairs with continuation lines. - * The catalogs are generated with `noWrap`, but Weblate still folds long - * strings, so continuations have to be joined. + * Minimal PO reader: `msgid`/`msgstr` pairs including `msgctxt`, plural forms + * and continuation lines. The catalogs are generated with `noWrap`, but Weblate + * still folds long strings, so continuations have to be joined. + * + * Plural forms are read even though no catalog has one yet. A parser that + * silently skips a shape it does not know turns every rule below into a rule + * that passed because it never ran — the same way the first version of this + * check passed by reading no catalogs at all. */ function parsePo(path) { const entries = []; let msgid = null; - let msgstr = null; + let plural = null; + let msgstrs = []; let field = null; const flush = () => { - if (msgid) entries.push({ msgid, msgstr: msgstr ?? "" }); + // The header entry has an empty `msgid` and carries no translatable markup. + if (msgid) entries.push({ msgid, plural, msgstrs }); msgid = null; - msgstr = null; + plural = null; + msgstrs = []; field = null; }; for (const raw of readFileSync(path, "utf8").split("\n")) { const line = raw.trim(); - if (line.startsWith("msgid ")) { - if (field === "msgstr") flush(); + let match; + if (line.startsWith("#")) { + // Comments, flags and `#~` obsolete entries carry nothing this check + // needs; an obsolete entry is not compiled into the catalog. + } else if (line.startsWith("msgctxt ")) { + flush(); + field = "msgctxt"; + } else if (line.startsWith("msgid ")) { + // A `msgctxt` line has already opened this entry. + if (field !== "msgctxt") flush(); msgid = unquote(line.slice(6)); field = "msgid"; - } else if (line.startsWith("msgstr ")) { - msgstr = unquote(line.slice(7)); - field = "msgstr"; + } else if (line.startsWith("msgid_plural ")) { + plural = unquote(line.slice(13)); + field = "msgid_plural"; + } else if ((match = /^msgstr(?:\[(\d+)\])?\s/.exec(line))) { + const index = match[1] ? Number(match[1]) : 0; + msgstrs[index] = unquote(line.slice(match[0].length)); + field = `msgstr${index}`; } else if (line.startsWith('"') && field) { - if (field === "msgid") msgid += unquote(line); - else msgstr += unquote(line); - } else if (line === "" && field === "msgstr") { + const text = unquote(line); + if (field === "msgid") msgid += text; + else if (field === "msgid_plural") plural += text; + else if (field.startsWith("msgstr")) msgstrs[Number(field.slice(6))] += text; + // `msgctxt` continuations disambiguate the entry, they are not rendered. + } else if (line === "" && field?.startsWith("msgstr")) { flush(); - } else if (line.startsWith("#")) { - // comment or flag line — carries no data this check needs } } flush(); - return entries.filter((entry) => entry.msgid !== ""); + return entries; } function unquote(value) { const trimmed = value.trim(); const inner = trimmed.startsWith('"') && trimmed.endsWith('"') ? trimmed.slice(1, -1) : trimmed; - return inner.replace(/\\"/g, '"').replace(/\\n/g, "\n").replace(/\\t/g, "\t").replace(/\\\\/g, "\\"); + // One pass, because chained replacements would rewrite the `n` of an escaped + // backslash: `\\n` is a backslash followed by the letter n, not a newline. + return inner.replace(/\\(.)/g, (_, char) => ESCAPES[char] ?? char); } -const TAG = /<\/?([a-zA-Z][a-zA-Z0-9]*)(?:\s[^>]*)?>/g; -const TT = /([\s\S]*?)<\/tt>/g; -const PLACEHOLDER = /%[sd]|%\d+\$[sd]|\{\w+\}|%\{\w+\}/g; - /** - * Rule 1 — the markup must parse. + * Splits `text` into markup tokens, reporting every shape Pango rejects and + * returning the tags it found, normalised for comparison. * - * Tutorial paragraphs are rendered through `GtkLabel use-markup` (see - * `packages/learn/tsx/components/gtk/`). Pango treats a malformed fragment as - * an error and leaves the label blank, so a broken `` silently - * costs the reader a whole paragraph. + * One scan feeds both rule 1 and rule 2 so the two cannot disagree about where + * a tag begins: a token that the balance check accepts but the tag comparison + * does not see is a gap wide enough to walk a dropped `` through. */ -function markupErrors(text) { - const stack = []; +function scanMarkup(text) { const errors = []; + const tags = []; + const stack = []; + + for (let index = text.indexOf("&"); index !== -1; index = text.indexOf("&", index + 1)) { + if (!ENTITY.test(text.slice(index))) + errors.push(`unescaped & or unknown entity: ${clip(text.slice(index, index + 16))}`); + } - // Reject the shapes a text parser would otherwise accept: a `<` that opens - // no valid tag, and an unquoted or unterminated attribute list. - for (const match of text.matchAll(/<[^>]*(?:>|$)/g)) { + for (const match of text.matchAll(TOKEN)) { const token = match[0]; - if (!token.endsWith(">")) errors.push(`unterminated tag: ${clip(token)}`); - else if (!/^<\/?[a-zA-Z][a-zA-Z0-9]*(?:\s+[a-zA-Z-]+="[^"]*")*\s*\/?>$/.test(token)) + if (!token.endsWith(">")) { + errors.push(`unterminated tag: ${clip(token)}`); + continue; + } + + const close = CLOSE_TAG.exec(token); + if (close) { + tags.push(``); + if (stack.pop() !== close[1]) errors.push(`unbalanced `); + continue; + } + + const open = OPEN_TAG.exec(token); + if (!open) { errors.push(`malformed tag: ${clip(token)}`); - } + continue; + } + + const [, name, attributeList, selfClosing] = open; + const attributes = [...attributeList.matchAll(ATTRIBUTE)].map((attribute) => attribute[1]); + const rules = TAG_ATTRIBUTES[name]; - for (const match of text.matchAll(TAG)) { - const name = match[1]; - if (match[0].startsWith("`); - } else if (!match[0].endsWith("/>")) { - stack.push(name); + if (!TAGS.has(name)) errors.push(`unsupported tag: <${name}>`); + for (const attribute of attributes) { + if (!rules?.allowed.has(attribute)) errors.push(`<${name}> does not support the ${attribute} attribute`); } + for (const attribute of rules?.required ?? []) { + if (!attributes.includes(attribute)) errors.push(`<${name}> is missing the ${attribute} attribute`); + } + + // An `` whose href is empty still parses, so Pango cannot report it; it + // renders as text the reader can click to nowhere. + const href = /href\s*=\s*["']([^"']*)["']/.exec(attributeList); + if (href && !href[1].trim()) errors.push(`<${name}> has an empty href`); + + // In-document anchors navigate inside the tutorial, so a rewritten fragment + // is a dead link rather than a localisation and has to survive verbatim. + // Every other href may be retargeted, as the README allows. + tags.push(href?.[1].startsWith("#") ? `` : `<${name}>`); + if (!selfClosing) stack.push(name); } + for (const name of stack) errors.push(`unclosed <${name}>`); - return errors; + return { errors, tags }; +} + +/** + * Rule 1 — the markup must parse. + * + * Tutorial paragraphs are rendered through `GtkLabel use-markup` (see + * `packages/learn/tsx/components/gtk/`). Pango treats a malformed fragment as + * an error and leaves the label blank, so a broken ``, an unescaped + * `&` or a tag Pango does not know silently costs the reader a whole paragraph. + */ +function markupErrors(text) { + return scanMarkup(text).errors; } /** @@ -129,18 +241,9 @@ function markupErrors(text) { * order legitimately moves an emphasised span, and the README lets a translator * point an `` at an equivalent page in the target language. What is not * allowed is dropping the span, which costs the emphasis or the whole link. - * - * In-document anchors are the exception — `` navigates - * inside the tutorial, so a rewritten fragment is a dead link, not a - * localisation. */ function tagCounts(text) { - return count( - [...text.matchAll(TAG)].map((match) => { - const fragment = /]*href="(#[^"]*)"/.exec(match[0]); - return fragment ? `` : match[0].replace(/^<(\/?)([a-zA-Z0-9]+).*$/, "<$1$2>"); - }) - ); + return count(scanMarkup(text).tags); } /** @@ -149,13 +252,21 @@ function tagCounts(text) { * These spans name subroutines, labels, opcodes and addresses that the reader * looks up in the editor. Translating `illegalMove` or * `0001 AND 0001` produces a tutorial that describes code the reader - * cannot find, and `JSR end` no longer assembles. + * cannot find, and `JSR end` no longer assembles. Matching a bare + * `` is exact rather than lax: Pango accepts no attribute on the tag, so + * rule 1 has already rejected any `` before this runs. */ function codeLiterals(text) { return count([...text.matchAll(TT)].map((match) => match[1]).filter((literal) => !PROSE_TT.has(literal))); } -/** Rule 4 — format placeholders must survive, or the string breaks at runtime. */ +/** + * Rule 4 — format placeholders must survive, and no new one may appear. + * + * A dropped placeholder loses the value it stood for; an added one consumes an + * argument that was never passed. Both break the string at runtime, so the + * comparison runs in both directions. + */ function placeholders(text) { return count([...text.matchAll(PLACEHOLDER)].map((match) => match[0])); } @@ -171,7 +282,7 @@ function missing(expected, actual) { const lost = []; for (const [value, times] of expected) { const short = times - (actual.get(value) ?? 0); - for (let i = 0; i < short; i++) lost.push(value); + for (let index = 0; index < short; index++) lost.push(value); } return lost; } @@ -180,50 +291,158 @@ function clip(text) { return text.length > 70 ? `${text.slice(0, 70)}…` : text; } +/** Applies every rule to one source/translation pair. */ +function entryProblems(source, translation) { + const problems = []; + + for (const error of markupErrors(translation)) problems.push(`invalid markup — ${error}`); + + const lostTags = missing(tagCounts(source), tagCounts(translation)); + if (lostTags.length) problems.push(`dropped tags — ${lostTags.map(clip).join(", ")}`); + + const lostCode = missing(codeLiterals(source), codeLiterals(translation)); + if (lostCode.length) + problems.push(`altered code literals — ${lostCode.map((literal) => `${clip(literal)}`).join(", ")}`); + + const sourcePlaceholders = placeholders(source); + const translated = placeholders(translation); + const lost = missing(sourcePlaceholders, translated); + if (lost.length) problems.push(`dropped placeholders — ${lost.join(", ")}`); + const added = missing(translated, sourcePlaceholders); + if (added.length) problems.push(`added placeholders — ${added.join(", ")}`); + + return problems; +} + +/** + * Pairs every rule must classify correctly, checked before any catalog is read. + * + * A structural check that has quietly stopped biting is worse than no check at + * all: it reports success over content nobody has looked at, which is exactly + * how the damage this file guards against reached `main` in the first place. + * The rejected shapes are the ones `Gtk.Label` renders as an empty string; the + * accepted ones are legitimate translations that must not be flagged. + */ +const SELF_TEST = [ + ["Hello world", "Hallo Welt", "accept"], + ["A then B", "B, dann A", "accept"], + [ + 'See this.', + 'Siehe dies.', + "accept", + ], + ['See this.', "Siehe dies.", "accept"], + // A right-to-left translation, with the RLM (U+200F) an RTL catalog may carry. + ["Use LDA here", "\u200fהשתמש ב־LDA כאן", "accept"], + ["A & B", "A & B", "accept"], + ["label marks a spot", "Marke markiert eine Stelle", "accept"], + ["Line %s of %d", "Zeile %s von %d", "accept"], + ["A and B", "A & B", "reject"], + ["A and B", "A B", "reject"], + ["x", "x", "reject"], + ['See this', "Siehe dies", "reject"], + ['See this', 'Siehe dies', "reject"], + ["x", 'x', "reject"], + ["x", "x y", "reject"], + ["LDA", 'LDA', "reject"], + ["illegalMove", "ungueltigerZug", "reject"], + ['jumping', 'Springen', "reject"], + ["A and B", "A und B", "reject"], + ["Line %s", "Zeile %s %s", "reject"], + ["Line %s", "Zeile", "reject"], +]; + +/** Self-test results that came out the wrong way round. */ +function selfTestFailures() { + const wrong = []; + for (const [source, translation, expected] of SELF_TEST) { + const problems = entryProblems(source, translation); + const verdict = problems.length ? "reject" : "accept"; + if (verdict !== expected) + wrong.push(`expected to ${expected}: ${clip(translation)} (${problems.join("; ") || "no problem reported"})`); + } + return wrong; +} + +/** The languages the build compiles, which is the set that must be checked. */ +function expectedLanguages() { + return new Set( + readFileSync(`${PO_DIR}/LINGUAS`, "utf8") + .split("\n") + .map((line) => line.replace(/#.*$/, "").trim()) + .filter(Boolean) + ); +} + function check() { - const failures = []; + const selfTest = selfTestFailures(); + if (selfTest.length) { + console.error("The structural rules no longer classify their own test cases:"); + for (const failure of selfTest) console.error(` ${failure}`); + return 1; + } + const catalogs = readCatalogs(); // A run that reads nothing passes every rule, which is the one failure this - // check must never report as success. LINGUAS is the count the build uses. - const expected = readFileSync(`${PO_DIR}/LINGUAS`, "utf8").split("\n").filter(Boolean).length; - if (catalogs.length !== expected) { - console.error(`Read ${catalogs.length} catalogs from ${PO_DIR}, but LINGUAS names ${expected}.`); + // check must never report as success. Names are compared, not just counted: + // a stray `.po` next to a missing one keeps the count right while leaving a + // shipped language unchecked. + const expected = expectedLanguages(); + const read = new Set(catalogs.map((catalog) => catalog.lang)); + const unread = [...expected].filter((lang) => !read.has(lang)); + const unlisted = [...read].filter((lang) => !expected.has(lang)); + if (unread.length || unlisted.length) { + if (unread.length) + console.error(`LINGUAS names ${unread.length} language(s) with no catalog in ${PO_DIR}: ${unread.join(", ")}`); + if (unlisted.length) + console.error(`${PO_DIR} holds ${unlisted.length} catalog(s) LINGUAS does not name: ${unlisted.join(", ")}`); return 1; } - for (const { lang, entries } of catalogs) { - for (const { msgid, msgstr } of entries) { - if (msgstr === "") continue; - const problems = []; - - for (const error of markupErrors(msgstr)) problems.push(`invalid markup — ${error}`); - - const lostTags = missing(tagCounts(msgid), tagCounts(msgstr)); - if (lostTags.length) problems.push(`dropped tags — ${lostTags.map(clip).join(", ")}`); - - const lostCode = missing(codeLiterals(msgid), codeLiterals(msgstr)); - if (lostCode.length) - problems.push(`altered code literals — ${lostCode.map((l) => `${clip(l)}`).join(", ")}`); + const failures = []; + // The English source is checked once rather than once per language: a broken + // fragment in `tutorial.mdx` reaches every catalog through the `.pot`. + const sources = new Set(); - const lostPlaceholders = missing(placeholders(msgid), placeholders(msgstr)); - if (lostPlaceholders.length) problems.push(`dropped placeholders — ${lostPlaceholders.join(", ")}`); + for (const { lang, entries } of catalogs) { + for (const { msgid, plural, msgstrs } of entries) { + sources.add(msgid); + if (plural) sources.add(plural); - if (problems.length) failures.push({ lang, msgid, problems }); + for (const [index, msgstr] of msgstrs.entries()) { + if (!msgstr) continue; + // Form 0 renders the singular, every later form the plural. + const source = index === 0 ? msgid : (plural ?? msgid); + const problems = entryProblems(source, msgstr); + if (problems.length) failures.push({ where: `${lang}.po`, msgid, problems }); + } } } - for (const { lang, msgid, problems } of failures) { - console.error(`${lang}.po: ${clip(msgid.replace(/\n/g, " "))}`); + for (const source of sources) { + const problems = markupErrors(source); + if (problems.length) + failures.push({ + where: "English source", + msgid: source, + problems: problems.map((error) => `invalid markup — ${error}`), + }); + } + + for (const { where, msgid, problems } of failures) { + console.error(`${where}: ${clip(msgid.replace(/\n/g, " "))}`); for (const problem of problems) console.error(` ${problem}`); } if (failures.length) { - console.error(`\n${failures.length} translation(s) failed structural validation.`); + console.error(`\n${failures.length} string(s) failed structural validation.`); return 1; } - console.log("All translations passed structural validation."); + console.log( + `All translations passed structural validation (${catalogs.length} catalogs, ${SELF_TEST.length} self-test cases).` + ); return 0; } From 5f4fd5b39ccb5f6554a51fae06b6cdd549a45da1 Mon Sep 17 00:00:00 2001 From: Pascal Garber Date: Fri, 4 Sep 2026 00:09:48 +0200 Subject: [PATCH 5/6] Translations: finish the Tamil half-English repair MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three entries of the same class were still on the branch. The structural check cannot see them — the markup and the literals are intact, only the prose is untranslated English with a word swapped in — so they were found by measuring how much of each msgid survives verbatim in its msgstr: Games now automatically இடைநிறுத்தம் when switching between screens $10-$15: Snake position தகவல்கள் (head, body segments, tail) இதோ ஒரு சான்று. குறிப்பு that immediate operands அரே still prefixed with a #. The last one also carries அரே, a transliteration of the English "are". Every other catalog translates all three. Each replacement is built from words the catalog already uses — பாம்பின் உடல் பகுதிகள் and தலை from the checkSnakeCollision paragraph, உடனடி and முன்னொட்ட- from the addressing-mode entries, இடைநிறுத்த- from the Pause button — and keeps the house style: no grantha letters, and the markup and literals untouched. Wording still needs a native speaker, like the rest of ta. Claude-Session: https://claude.ai/code/session_01URLYj1wgGxVNRbBpNT8xAV --- packages/translations/ta.po | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/translations/ta.po b/packages/translations/ta.po index 3128fdd5..1ea93e70 100644 --- a/packages/translations/ta.po +++ b/packages/translations/ta.po @@ -58,7 +58,7 @@ msgstr "$0: கருப்பு (#000000)" #. TRANSLATORS: MDX-derived text from packages/learn/quick-help.mdx msgid "$10-$15: Snake position data (head, body segments, tail)" -msgstr "$10-$15: Snake position தகவல்கள் (head, body segments, tail)" +msgstr "$10-$15: பாம்பின் இருப்பிடத் தரவு (தலை, உடல் பகுதிகள், வால்)" #. TRANSLATORS: MDX-derived text from packages/learn/quick-help.mdx msgid "$1: White (#ffffff)" @@ -815,7 +815,7 @@ msgstr "கேம்பேட் விசை அழுத்தப்பட் #. TRANSLATORS: Release notes for version 0.2.0 - auto pause msgid "Games now automatically pause when switching between screens" -msgstr "Games now automatically இடைநிறுத்தம் when switching between screens" +msgstr "திரைகளுக்கு இடையே மாறும்போது கேம்கள் இப்போது தானாகவே இடைநிறுத்தப்படும்" #. TRANSLATORS: Note about validation status - keep it concise and encouraging msgid "German and Spanish have been validated by contributors. Other languages are AI‑assisted drafts and need human validation." @@ -854,7 +854,7 @@ msgstr "உதவி" #. TRANSLATORS: MDX-derived text from packages/learn/tutorial.mdx msgid "Here's an example. Note that immediate operands are still prefixed with a #." -msgstr "இதோ ஒரு சான்று. குறிப்பு that immediate operands அரே still prefixed with a #." +msgstr "இதோ ஒரு சான்று. உடனடி மதிப்புகள் இன்னும் # முன்னொட்டுடன் இருப்பதைக் கவனிக்கவும்." #. TRANSLATORS: Section heading label for the Hex Monitor block msgid "Hex Monitor" From 7d6b47388fe903c04800ac27be1ec4c21238caed Mon Sep 17 00:00:00 2001 From: Pascal Garber Date: Fri, 4 Sep 2026 00:11:31 +0200 Subject: [PATCH 6/6] Translations: document what the check covers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The README said the script catches "both" — unparseable markup and a translated code identifier. It now names the four rules, so a translator reading it knows what will be rejected and why, and the entity rule gets its own line under external links: a link retargeted to a URL joined with a bare `&` blanks the paragraph, which is the least obvious of the traps. AGENTS.md still pointed only at `build` for validation. `build` rewrites every catalog as a side effect; `check` is the one that reads them. Claude-Session: https://claude.ai/code/session_01URLYj1wgGxVNRbBpNT8xAV --- AGENTS.md | 2 +- packages/translations/README.md | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 774dccc9..f2591f6e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -335,7 +335,7 @@ Persona: native speaker + software engineer + 6502/retro dev interest. Produce a **Reference:** Other language files in `packages/translations/*.po` (de, es, fr, ia, nl) as context. -**Validate:** `gjsify workspace @learn6502/translations build` +**Validate:** `gjsify workspace @learn6502/translations check` (structure: markup, tags, `` literals, placeholders — also run in CI), then `build` ## Documentation diff --git a/packages/translations/README.md b/packages/translations/README.md index d0588c3b..5da4790c 100644 --- a/packages/translations/README.md +++ b/packages/translations/README.md @@ -45,6 +45,8 @@ Support for every new language is welcome. - You may replace links to English pages with equivalent high-quality pages in the target language. - Prefer reputable sources (e.g., Wikipedia) when a high-quality article exists in the target language. - In-document anchors like `href="#jumping"` navigate inside the tutorial — keep the fragment as it is. + - Write `&` as `&`, in a URL as well as in prose. A bare `&` — or an HTML-only entity like + ` ` — is a markup error, and the GNOME app renders the whole paragraph as nothing. 4. **Inline `` is code**, not prose: subroutine names, labels, opcodes, registers and addresses the reader looks up in the editor. Copy it through unchanged. A translated `illegalMove` @@ -59,9 +61,16 @@ gjsify workspace @learn6502/translations check ``` `msgfmt` only asks whether a `msgstr` is non-empty, so a catalog can report 100% translated while -shipping markup that Pango refuses to parse — which blanks the whole paragraph in the GNOME app — -or a code identifier that was translated. The `check` script catches both, and CI runs it on every -pull request. It says nothing about language quality; that still needs a native speaker. +shipping strings the GNOME app cannot render. The `check` script reads what `msgfmt` does not, and +CI runs it on every pull request: + +- the markup parses — every tag, attribute and `&` entity is one `GtkLabel` accepts, because a label + whose markup fails to parse renders as nothing at all; +- every tag the English opens is still there, so no emphasis and no link is dropped in translation; +- `` content is unchanged, per the rule above; +- format placeholders match — none dropped, none added. + +It says nothing about language quality; that still needs a native speaker. ## Resources