localise
Lokalise Translation Generator
Generate an interactive HTML translation page for pasting translations into Lokalise.
When to Use
- User provides an English string (with or without plural forms) and wants translations for Lokalise
- User mentions translating a key for the app's supported languages
- User needs a quick translation reference with copy-to-clipboard functionality
Input
Parse $ARGUMENTS as the English text to translate. The format is <english singular> [| <english plural>].
- If
$ARGUMENTScontains a|separator, split into singular (before|) and plural (after|) - If
$ARGUMENTShas no|, treat the entire string as the singular (other) form for all languages - If
$ARGUMENTSis empty, useAskUserQuestionto ask the user for the English text
The user may also provide:
- Key name (optional): e.g.
tables::selected_count. If not given, derive from the English string. - Variables: Strings may contain
%{variable_name}interpolation tokens. Preserve these exactly in all translations.
Target Languages (in this exact order)
| # | Code | Name | Plural Forms (Lokalise) | RTL |
|---|---|---|---|---|
| 1 | en | English | one, other | no |
| 2 | ar | Arabic | zero, one, two, few, many, other | yes |
| 3 | zh-CN | Chinese (Simplified) | other | no |
| 4 | zh-TW | Chinese (Traditional) | other | no |
| 5 | nl | Dutch | one, other | no |
| 6 | fr | French | one, other | no |
| 7 | de | German | one, other | no |
| 8 | he | Hebrew | one, other | yes |
| 9 | hi | Hindi | one, other | no |
| 10 | id | Indonesian | other | no |
| 11 | it | Italian | one, other | no |
| 12 | ja | Japanese | other | no |
| 13 | km | Khmer | other | no |
| 14 | ko | Korean | other | no |
| 15 | lo | Lao | other | no |
| 16 | ms | Malay | other | no |
| 17 | pt | Portuguese | one, other | no |
| 18 | ru | Russian | one, few, many, other | no |
| 19 | es | Spanish | one, other | no |
| 20 | tl | Tagalog | zero, one, two, few, many, other | no |
| 21 | th | Thai | other | no |
| 22 | tr | Turkish | one, other | no |
| 23 | vi | Vietnamese | other | no |
Translation Guidelines
- Preserve all
%{...}interpolation variables exactly as-is in every translation. - For languages with only
other: provide a single translation (no plural distinction). - For languages with
one/other: provide singular and plural forms where grammatically appropriate. If the language doesn't change the noun (e.g. Hindi, Turkish for some words), the forms may be identical — that's fine. - For Russian (
one/few/many/other): follow Russian plural declension rules. - For Arabic (
zero/one/two/few/many/other): follow Arabic plural rules.zeroform can omit the count variable if natural (e.g. "No items selected"). - For Tagalog (
zero/one/two/few/many/other): follow Tagalog linker rules.zeroform can omit the count variable. - RTL languages (Arabic, Hebrew) get the
rtlCSS class on their translation rows. - Translations should sound natural in a restaurant/hospitality UI context (TableCheck product).
Output
Generate a single self-contained HTML file.
File Location
Save the file to: {working_directory}/.tmp/translations-{sanitized_key}.html
Where {sanitized_key} is the key name (or derived slug) with special characters replaced by hyphens.
After generating, open the file with the default system browser:
# macOS
open "{filepath}"
# Linux
xdg-open "{filepath}"
# Try open first, fall back to xdg-open
open "{filepath}" 2>/dev/null || xdg-open "{filepath}"
HTML Structure
The generated HTML MUST follow this exact structure. Copy the template below precisely, only changing the dynamic content (key name, translations).
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{KEY_NAME} — Plural Translations</title>
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>
:root {
--bg: #0e1117;
--surface: #161b22;
--surface-2: #1c2129;
--border: #2d333b;
--border-light: #373e47;
--text: #e6edf3;
--text-muted: #8b949e;
--accent: #58a6ff;
--accent-dim: #1a3a5c;
--green: #3fb950;
--green-dim: #1a3524;
--yellow: #d29922;
--yellow-dim: #3d2e00;
--purple: #bc8cff;
--purple-dim: #2a1a4e;
--orange: #f0883e;
--red: #f85149;
--code-bg: #1a1f27;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: var(--bg);
color: var(--text);
font-family: 'DM Sans', sans-serif;
padding: 40px 32px;
line-height: 1.6;
}
.container {
max-width: 1360px;
margin: 0 auto;
}
header { margin-bottom: 36px; }
h1 {
font-size: 22px;
font-weight: 700;
color: var(--text);
margin-bottom: 6px;
display: flex;
align-items: center;
gap: 10px;
}
h1 code {
font-family: 'JetBrains Mono', monospace;
font-size: 16px;
background: var(--accent-dim);
color: var(--accent);
padding: 3px 10px;
border-radius: 6px;
font-weight: 500;
}
.subtitle { color: var(--text-muted); font-size: 14px; }
.legend { display: flex; gap: 20px; margin-top: 14px; flex-wrap: wrap; }
.legend-item { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--text-muted); }
.legend-dot { width: 10px; height: 10px; border-radius: 3px; }
.legend-dot.one { background: var(--green); }
.legend-dot.other { background: var(--accent); }
.legend-dot.few { background: var(--yellow); }
.legend-dot.many { background: var(--purple); }
.legend-dot.two { background: var(--orange); }
.legend-dot.zero { background: var(--red); }
table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
border: 1px solid var(--border);
border-radius: 10px;
overflow: hidden;
font-size: 14px;
}
thead th {
background: var(--surface-2);
color: var(--text-muted);
font-weight: 600;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.8px;
padding: 12px 16px;
text-align: left;
border-bottom: 1px solid var(--border);
position: sticky;
top: 0;
z-index: 2;
}
thead th:first-child { width: 50px; }
thead th:nth-child(2) { width: 140px; }
thead th:nth-child(3) { width: 100px; }
tbody tr { transition: background 0.15s; }
tbody tr:hover { background: var(--surface); }
tbody tr:not(:last-child) td { border-bottom: 1px solid var(--border); }
td { padding: 10px 16px; vertical-align: top; }
.lang-code { font-family: 'JetBrains Mono', monospace; font-size: 13px; font-weight: 500; color: var(--accent); }
.lang-name { font-weight: 500; color: var(--text); font-size: 13px; }
.lang-name span { color: var(--text-muted); font-weight: 400; font-size: 12px; display: block; }
.form-tag {
display: inline-block;
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
font-weight: 500;
padding: 2px 8px;
border-radius: 4px;
min-width: 46px;
text-align: center;
flex-shrink: 0;
}
.form-tag.one { background: var(--green-dim); color: var(--green); }
.form-tag.other { background: var(--accent-dim); color: var(--accent); }
.form-tag.few { background: var(--yellow-dim); color: var(--yellow); }
.form-tag.many { background: var(--purple-dim); color: var(--purple); }
.form-tag.two { background: #3d1f00; color: var(--orange); }
.form-tag.zero { background: #3d1116; color: var(--red); }
.translation-cell { display: flex; flex-direction: column; gap: 6px; }
.translation-row {
display: flex;
align-items: center;
gap: 10px;
}
.translation-text {
font-family: 'JetBrains Mono', monospace;
font-size: 13px;
color: var(--text);
flex: 1;
}
.translation-text .var { color: var(--yellow); }
.copy-btn {
flex-shrink: 0;
background: var(--surface-2);
border: 1px solid var(--border);
color: var(--text-muted);
font-family: 'DM Sans', sans-serif;
font-size: 11px;
font-weight: 500;
padding: 4px 10px;
border-radius: 5px;
cursor: pointer;
transition: all 0.15s;
display: flex;
align-items: center;
gap: 4px;
white-space: nowrap;
}
.copy-btn:hover {
background: var(--border);
color: var(--text);
border-color: var(--border-light);
}
.copy-btn:active { transform: scale(0.96); }
.copy-btn.copied {
background: var(--green-dim);
border-color: var(--green);
color: var(--green);
}
.copy-btn svg { width: 13px; height: 13px; }
.translation-row.handled {
background: rgba(248, 81, 73, 0.2);
border-radius: 4px;
padding: 2px 6px;
margin: -2px -6px;
}
.rtl .translation-text { direction: rtl; text-align: right; }
footer {
margin-top: 24px;
padding: 16px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 10px;
font-size: 12px;
color: var(--text-muted);
line-height: 1.8;
}
footer strong { color: var(--text); font-weight: 600; }
footer code {
font-family: 'JetBrains Mono', monospace;
background: var(--code-bg);
padding: 1px 5px;
border-radius: 3px;
font-size: 11px;
}
.toast {
position: fixed;
bottom: 24px;
right: 24px;
background: var(--green-dim);
border: 1px solid var(--green);
color: var(--green);
font-family: 'DM Sans', sans-serif;
font-size: 13px;
font-weight: 500;
padding: 10px 18px;
border-radius: 8px;
opacity: 0;
transform: translateY(10px);
transition: all 0.25s ease;
pointer-events: none;
z-index: 100;
display: flex;
align-items: center;
gap: 6px;
}
.toast.show { opacity: 1; transform: translateY(0); }
</style>
</head>
<body>
<div class="container">
<header>
<h1>Translations for <code>{KEY_NAME}</code></h1>
<p class="subtitle">23 languages · CLDR plural rules · Variable: <code style="font-family:'JetBrains Mono',monospace;background:var(--yellow-dim);color:var(--yellow);padding:1px 6px;border-radius:4px;font-size:13px">%{count}</code></p>
<div class="legend">
<div class="legend-item"><div class="legend-dot one"></div> one</div>
<div class="legend-item"><div class="legend-dot other"></div> other</div>
<div class="legend-item"><div class="legend-dot few"></div> few</div>
<div class="legend-item"><div class="legend-dot many"></div> many</div>
<div class="legend-item"><div class="legend-dot two"></div> two</div>
<div class="legend-item"><div class="legend-dot zero"></div> zero</div>
</div>
</header>
<table>
<thead>
<tr>
<th>#</th>
<th>Language</th>
<th>Forms</th>
<th>Translations</th>
</tr>
</thead>
<tbody>
<!-- LANGUAGE ROWS GO HERE (see Row Templates below) -->
</tbody>
</table>
<footer>
<strong>Notes:</strong><br>
• These translations should be reviewed by native speakers before production use.<br>
• Plural forms follow CLDR plural rules as configured in Lokalise.<br>
• RTL languages (Arabic, Hebrew) are displayed right-to-left.
</footer>
</div>
<div class="toast" id="toast">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="20 6 9 17 4 12"/></svg>
<span id="toast-text">Copied!</span>
</div>
<script>
const COPY_ICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>';
const CHECK_ICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" width="13" height="13"><polyline points="20 6 9 17 4 12"/></svg>';
let toastTimeout;
function copyText(btn) {
const row = btn.closest('.translation-row');
const textEl = row.querySelector('.translation-text');
const text = textEl.getAttribute('data-copy');
navigator.clipboard.writeText(text).then(() => {
showFeedback(btn, text);
}).catch(() => {
const ta = document.createElement('textarea');
ta.value = text;
ta.style.cssText = 'position:fixed;opacity:0';
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
showFeedback(btn, text);
});
}
function showFeedback(btn, text) {
const row = btn.closest('.translation-row');
row.classList.add('handled');
btn.classList.add('copied');
btn.innerHTML = CHECK_ICON + 'Copied!';
setTimeout(() => {
btn.classList.remove('copied');
btn.innerHTML = COPY_ICON + 'Copy';
}, 1500);
const toast = document.getElementById('toast');
const toastText = document.getElementById('toast-text');
const display = text.length > 45 ? text.substring(0, 45) + '…' : text;
toastText.textContent = 'Copied: ' + display;
clearTimeout(toastTimeout);
toast.classList.add('show');
toastTimeout = setTimeout(() => toast.classList.remove('show'), 2000);
}
</script>
</body>
</html>
Row Templates
Use these templates to build each language row. Replace {N}, {LANG_NAME}, {LANG_CODE}, and translation text.
The copy button HTML (reused everywhere):
<button class="copy-btn" onclick="copyText(this)"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>Copy</button>
Single form (other only)
For: zh-CN, zh-TW, id, ja, km, ko, lo, ms, th, vi
<tr>
<td><span class="lang-code">{N}</span></td>
<td><span class="lang-name">{LANG_NAME}<span>{LANG_CODE}</span></span></td>
<td><span class="form-tag other">other</span></td>
<td class="translation-cell">
<div class="translation-row">
<span class="translation-text" data-copy="{PLAIN_TEXT}">{DISPLAY_HTML}</span>
{COPY_BUTTON}
</div>
</td>
</tr>
Two forms (one / other)
For: en, nl, fr, de, he, hi, it, pt, es, tr
<tr>
<td><span class="lang-code">{N}</span></td>
<td><span class="lang-name">{LANG_NAME}<span>{LANG_CODE}</span></span></td>
<td><span class="form-tag one">one</span> <span class="form-tag other">other</span></td>
<td class="translation-cell">
<div class="translation-row{RTL_CLASS}">
<span class="form-tag one">one</span>
<span class="translation-text" data-copy="{PLAIN_ONE}">{DISPLAY_ONE}</span>
{COPY_BUTTON}
</div>
<div class="translation-row{RTL_CLASS}">
<span class="form-tag other">other</span>
<span class="translation-text" data-copy="{PLAIN_OTHER}">{DISPLAY_OTHER}</span>
{COPY_BUTTON}
</div>
</td>
</tr>
Four forms (one / few / many / other)
For: ru
<tr>
<td><span class="lang-code">{N}</span></td>
<td><span class="lang-name">Russian<span>ru</span></span></td>
<td><span class="form-tag one">one</span> <span class="form-tag few">few</span> <span class="form-tag many">many</span> <span class="form-tag other">other</span></td>
<td class="translation-cell">
<div class="translation-row">
<span class="form-tag one">one</span>
<span class="translation-text" data-copy="{PLAIN}">{DISPLAY}</span>
{COPY_BUTTON}
</div>
<div class="translation-row">
<span class="form-tag few">few</span>
<span class="translation-text" data-copy="{PLAIN}">{DISPLAY}</span>
{COPY_BUTTON}
</div>
<div class="translation-row">
<span class="form-tag many">many</span>
<span class="translation-text" data-copy="{PLAIN}">{DISPLAY}</span>
{COPY_BUTTON}
</div>
<div class="translation-row">
<span class="form-tag other">other</span>
<span class="translation-text" data-copy="{PLAIN}">{DISPLAY}</span>
{COPY_BUTTON}
</div>
</td>
</tr>
Six forms (zero / one / two / few / many / other)
For: ar, tl
<tr>
<td><span class="lang-code">{N}</span></td>
<td><span class="lang-name">{LANG_NAME}<span>{LANG_CODE}</span></span></td>
<td><span class="form-tag zero">zero</span> <span class="form-tag one">one</span> <span class="form-tag two">two</span> <span class="form-tag few">few</span> <span class="form-tag many">many</span> <span class="form-tag other">other</span></td>
<td class="translation-cell">
<!-- One div.translation-row per form: zero, one, two, few, many, other -->
<!-- Add " rtl" to class for Arabic: class="translation-row rtl" -->
<div class="translation-row{RTL_CLASS}">
<span class="form-tag zero">zero</span>
<span class="translation-text" data-copy="{PLAIN}">{DISPLAY}</span>
{COPY_BUTTON}
</div>
<!-- ... repeat for one, two, few, many, other -->
</td>
</tr>
Display HTML Formatting
In the {DISPLAY} content, wrap any %{variable} tokens with the yellow highlight span:
- Plain text (for
data-copy):%{count} tables selected - Display HTML (for visible content):
<span class="var">%{count}</span> tables selected
Markdown File
In addition to the HTML file, generate a companion Markdown file containing all translations in a table format.
File Location
Save the file to: {working_directory}/.tmp/translations-{sanitized_key}.md
(Same directory and naming as the HTML file, but with .md extension.)
Markdown Structure
The file MUST follow this exact structure:
# Translations: `{KEY_NAME}`
| # | Language | Code | Form | Translation |
|---|----------|------|------|-------------|
| 1 | English | en | one | %{count} table selected |
| 1 | English | en | other | %{count} tables selected |
| 2 | Arabic | ar | zero | ... |
| 2 | Arabic | ar | one | ... |
| 2 | Arabic | ar | two | ... |
| 2 | Arabic | ar | few | ... |
| 2 | Arabic | ar | many | ... |
| 2 | Arabic | ar | other | ... |
| 3 | Chinese (Simplified) | zh-CN | other | ... |
...
Rules:
- Languages appear in the same order as the HTML (1-23)
- Each plural form gets its own row
- The
#column uses the same row number for all forms of a language (e.g., all Arabic forms are2) - The
Translationcolumn contains plain text (with%{...}variables preserved, no HTML markup) - For languages with a single form (
otheronly), there is one row - For languages with multiple forms, each form gets a separate row in the order: zero, one, two, few, many, other (only the forms that apply to that language)
Checklist Before Saving
- All 23 languages present in the exact order specified
- Correct plural forms per language (not generic CLDR — use the Lokalise forms table above)
%{...}variables preserved exactly in all translationsdata-copyattributes contain plain text (no HTML)- Display content has
<span class="var">around variables - RTL class applied to Arabic and Hebrew rows
- Row numbers sequential 1-23
- Copy buttons on every translation row
- Both HTML and Markdown files saved to
.tmp/folder - HTML file opened in default browser after generation
More from morphet81/cheat-sheets
translate-pdf
Translate a PDF document from one language to another. Extracts text to structured Markdown, translates it, and builds a new translated PDF. Requires a Python environment with pymupdf, markdown, and weasyprint.
67create-jira-ticket
Create a JIRA ticket from user instructions via acli. Uses project from the current branch when possible, lists project epics, recommends the best epic, asks confirmation before creating, uses ADF descriptions, and can attach Figma designs via the Jira integration.
67adb
Use ADB to interact with an Android device or emulator. Takes a screenshot, understands the screen, performs actions (tap, swipe, type, navigate), and loops until the mission is complete.
66pre-push
Run pre-push checks including tests and linting to ensure code is clean and ready to push. Automatically detects project type and available scripts. Runs independent checks in parallel using agents.
66update-jira-ticket
Compare the JIRA ticket description to changes made in the current branch and propose description edits and/or comments to keep the ticket accurate and well-documented.
66verify-test-cases
Verify test cases in all test files modified since branching out from base branch. Checks that test cases make sense, have no duplications, and provide meaningful coverage. Spawns parallel agents for multi-file analysis. After the user confirms test-case changes, runs coverage (npm test:coverage or Jest/Vitest fallback) and fixes tests until coverage passes.
66