diff --git a/apps/mint/README.md b/apps/mint/README.md index 0e2c5486..ddee70a8 100644 --- a/apps/mint/README.md +++ b/apps/mint/README.md @@ -119,6 +119,14 @@ API `https://mint-api.juw12.workers.dev/api/health` formulas follow the renamed tabs. - **Links pick the language (3.5.1).** Add `?lang=en|id|zh|es` to any Mint link (Budget, the marketplace, the legal pages) and it opens in that language without changing the reader's saved choice. +- **Every Studio reply, every language (3.5.4).** The rarer replies are translated too (`列“供应商”已存在。`, + `Rumus salah: kolom [Foo] tidak ada.`, `Ya hay otra columna llamada “Precio”.`), Indonesian gets highlights and + card words (`sorot Laba kurang dari 0 merah`, `rata-rata Jumlah`), and the “didn’t understand” hint suggests + commands in your language that really work. Describing your own tracker works in Chinese and Spanish too + (`建一个装修记录表,列:项目、费用、日期`, `columnas: Producto, Precio y Fecha`), and the example chips send the + words you see on them. `add a row` works without a number in every language (`加一行`, `añade una fila`, + `tambah baris`), Chinese highlights can name the column first (`利润 小于 0 标红`), and Spanish theme names work + (`cambia el tema a medianoche`). - **Studio speaks your language all the way (3.5.3).** Replies no longer leave English bits behind: theme names (`主题已设为午夜(深色)`, `Tema cambiado a Bosque`), column types (`teks`, `数字`, `fecha`) and card functions are translated, and a dashboard card added by a command (`合计 数量`, `promedio de cantidad`) is labelled in the diff --git a/apps/mint/desktop/mint.spec b/apps/mint/desktop/mint.spec index 323eb593..1d40c4d1 100644 --- a/apps/mint/desktop/mint.spec +++ b/apps/mint/desktop/mint.spec @@ -6,7 +6,7 @@ import os import sys DESKTOP = os.path.dirname(os.path.abspath(SPEC)) # noqa: F821 (provided by PyInstaller) -VERSION = os.environ.get("MINT_VERSION", "3.5.3") +VERSION = os.environ.get("MINT_VERSION", "3.5.4") VERSION_FILE = os.environ.get("MINT_VERSION_FILE") or None ICON = os.environ.get("MINT_ICON") or None IS_WIN = sys.platform == "win32" diff --git a/apps/mint/desktop/mint_app.py b/apps/mint/desktop/mint_app.py index 3379f604..40961213 100644 --- a/apps/mint/desktop/mint_app.py +++ b/apps/mint/desktop/mint_app.py @@ -30,7 +30,7 @@ import urllib.request import webbrowser -VERSION = "3.5.3" +VERSION = "3.5.4" TITLE = "Mint" APP_URL = os.environ.get("MINT_DESKTOP_URL") or "https://juw12.github.io/projects/mint/" DEFAULT_SIZE = (1280, 860) diff --git a/apps/mint/tests/test_core.py b/apps/mint/tests/test_core.py index 8aa22e1b..b953b4b8 100644 --- a/apps/mint/tests/test_core.py +++ b/apps/mint/tests/test_core.py @@ -218,7 +218,23 @@ def seed_checks(): esRemove: tEsRemove.spec.sheet.columns.map(function (c) { return c.name; }), esSum: tEsSum.spec.summary.map(function (s) { return s.fn + ":" + s.column; }), esCur: tEsCur.spec.currency, - zhGenTheme: C.generate("网店库存表,绿色").spec.theme + zhGenTheme: C.generate("网店库存表,绿色").spec.theme, + // 3.5.4: Indonesian highlight and card words + idRule: C.applyTweak(budget, "sorot Actual lebih dari 500 merah").spec.rules.slice(-1) + .map(function (k) { return k.column + k.op + k.value + ":" + k.style; })[0], + idAvg: C.applyTweak(budget, "rata-rata Actual").spec.summary.slice(-1).map(function (s) { return s.fn + ":" + s.column; })[0], + zhListed: C.generate("建一个互助会记录表,列:姓名、日期、存款").spec.sheet.columns.map(function (c) { return c.name + ":" + c.type; }), + esListed: C.generate("Crea una plantilla de tanda con columnas: Nombre, Fecha y Aporte").spec.sheet.columns.map(function (c) { return c.name + ":" + c.type; }), + zhListedTitle: C.generate("建一个互助会记录表,列:姓名、日期、存款").spec.title, + zhListedOwn: C.generate("建一个装修记录表,列:项目、费用、日期").spec.blueprint, + oneRow: ["add a row", "加一行", "añade una fila", "tambah baris"].map(function (cmd) { + return C.applyTweak(budget, cmd).spec.sheet.rows.length - budget.sheet.rows.length; + }), + zhColumnFirst: C.applyTweak(budget, "Actual 小于 0 标红").spec.rules.slice(-1) + .map(function (k) { return k.column + k.op + k.value + ":" + k.style; })[0], + esThemeWord: C.applyTweak(budget, "cambia el tema a medianoche").spec.theme, + enStill: C.applyTweak(budget, "warnai Actual below 0 red").spec.rules.slice(-1) + .map(function (k) { return k.column + k.op + k.value + ":" + k.style; })[0] }; out.tweaks = { rows: t1.spec.sheet.rows.length, @@ -376,6 +392,16 @@ def js_checks(): check("SUM:Actual" in intl["esSum"], "es 'suma de'") check(intl["esCur"] == "MXN", "es currency words") check(intl["zhGenTheme"] == "forest", "zh colour word picks the theme in prompts") + check(intl["idRule"] == "Actual>500:bad", "id 'sorot … lebih dari … merah' " + str(intl["idRule"])) + check(intl["idAvg"] == "AVERAGE:Actual", "id 'rata-rata' card " + str(intl["idAvg"])) + check(intl["enStill"] == "Actual<0:bad", "English 'warnai … below' still works " + str(intl["enStill"])) + check(intl["zhListed"] == ["姓名:text", "日期:date", "存款:currency"], "zh '列:' column list with types " + str(intl["zhListed"])) + check(intl["esListed"] == ["Nombre:text", "Fecha:date", "Aporte:currency"], "es 'con columnas:' list with types " + str(intl["esListed"])) + check(intl["zhListedTitle"] == "互助会记录表", "zh custom title stops before the column list " + intl["zhListedTitle"]) + check(intl["zhListedOwn"] == "custom", "a listed column called 项目 does not pick the project blueprint " + intl["zhListedOwn"]) + check(intl["oneRow"] == [1, 1, 1, 1], "add a row without a number works in en/zh/es/id " + str(intl["oneRow"])) + check(intl["zhColumnFirst"] == "Actual<0:bad", "zh column-first highlight 标红 " + str(intl["zhColumnFirst"])) + check(intl["esThemeWord"] == "midnight", "es theme word medianoche " + str(intl["esThemeWord"])) check(out["diffRows"] == [p[0] - p[1] for p in pairs], "Diff column evaluates per row") f = out["formulas"] diff --git a/apps/mint/web/budget/js/core.js b/apps/mint/web/budget/js/core.js index 2aee059b..c5a33c8c 100644 --- a/apps/mint/web/budget/js/core.js +++ b/apps/mint/web/budget/js/core.js @@ -8,7 +8,7 @@ (function (root) { 'use strict'; const K = (root.MintBudget = root.MintBudget || {}); - K.VERSION = '3.5.3'; + K.VERSION = '3.5.4'; /** Compare dotted versions numerically: 1, 0 or -1 ("1.10.0" is newer than "1.9.9"). */ K.compareVersions = function (a, b) { diff --git a/apps/mint/web/budget/sw.js b/apps/mint/web/budget/sw.js index cf032f78..209b6413 100644 --- a/apps/mint/web/budget/sw.js +++ b/apps/mint/web/budget/sw.js @@ -1,6 +1,6 @@ /* Mint Budget service worker: network-first app shell so updates land immediately, cache as offline fallback. * Scope is budget/ only; Mint Templates (../app.html) has its own worker at ../sw.js. */ -const CACHE = 'mint-budget-v18'; +const CACHE = 'mint-budget-v19'; // Caches this worker owns: its own generations, plus the ones versions before Mint 3.0 left behind. // Never touch other caches: every app on this origin shares one CacheStorage. const OWN_CACHE = /^(mint-budget-v|kitty-v)/; diff --git a/apps/mint/web/budget/version.json b/apps/mint/web/budget/version.json index ee23b8e2..78b07549 100644 --- a/apps/mint/web/budget/version.json +++ b/apps/mint/web/budget/version.json @@ -1,46 +1,46 @@ { - "version": "3.5.3", + "version": "3.5.4", "released": "2026-11-09", "notes": { "en": [ + "Templates Studio answers every message in your language, including “that column already exists” and formula errors, and understands Indonesian highlights like “sorot Laba kurang dari 0 merah”.", "Templates Studio answers fully in your language: theme names, column types and the dashboard cards a command adds (like 数量合计 or Promedio de cantidad) no longer slip into English.", "Templates come out in your language: a prompt in Indonesian, Chinese or Spanish gives column names, dropdowns, cards and sample rows in that language, with every formula still working.", "Links can choose the language: add ?lang=es (or id, zh, en) and Budget, the marketplace and the legal pages open in it, without changing your saved choice.", "Templates Studio understands Chinese and Spanish commands, like 添加供应商列 or añade la columna Proveedor.", "Live prices in Invest: stocks and ETFs (IDX and US), crypto and gold update from the market. Tap Refresh prices any time; prices you type yourself stay until you refresh.", "Mint asks your browser to keep its data, shows when you last backed up, and reminds you to sync or back up once a budget has real history.", - "Templates and the Marketplace now speak Chinese and Spanish too, and downloaded .xlsx files use your language for tabs and tips.", - "Privacy and Terms are available in Indonesian, Chinese and Spanish." + "Templates and the Marketplace now speak Chinese and Spanish too, and downloaded .xlsx files use your language for tabs and tips." ], "id": [ + "Studio Template menjawab setiap pesan dalam bahasamu, termasuk “kolom sudah ada” dan kesalahan rumus, dan kini paham perintah sorot seperti “sorot Laba kurang dari 0 merah”.", "Studio Template menjawab sepenuhnya dalam bahasamu: nama tema, jenis kolom, dan kartu dashboard yang ditambahkan lewat perintah tidak lagi muncul dalam bahasa Inggris.", "Template kini keluar dalam bahasamu: perintah dalam Bahasa Indonesia, Mandarin, atau Spanyol menghasilkan nama kolom, pilihan, kartu, dan contoh baris dalam bahasa itu, dengan semua rumus tetap berjalan.", "Tautan bisa memilih bahasa: tambahkan ?lang=id (atau en, zh, es) dan Budget, marketplace, serta halaman legal terbuka dalam bahasa itu tanpa mengubah pilihan tersimpanmu.", "Studio Template kini memahami perintah berbahasa Mandarin dan Spanyol, seperti 添加供应商列 atau añade la columna Proveedor.", "Harga live di Investasi: saham dan ETF (BEI dan AS), kripto, dan emas mengikuti pasar. Ketuk Perbarui harga kapan saja; harga yang kamu isi sendiri tetap sampai kamu memperbarui.", "Mint meminta browser menyimpan datanya, menampilkan kapan terakhir kamu mencadangkan, dan mengingatkan untuk sinkron atau mencadangkan saat anggaran sudah berisi banyak riwayat.", - "Template dan Marketplace kini juga berbahasa Mandarin dan Spanyol, dan file .xlsx yang diunduh memakai bahasamu untuk tab dan petunjuk.", - "Privasi dan Ketentuan tersedia dalam bahasa Indonesia, Mandarin, dan Spanyol." + "Template dan Marketplace kini juga berbahasa Mandarin dan Spanyol, dan file .xlsx yang diunduh memakai bahasamu untuk tab dan petunjuk." ], "zh": [ + "模板工作室的每条回复都使用你的语言,包括“列已存在”和公式错误提示;也能听懂印尼语的高亮指令。", "模板工作室完全用你的语言回复:主题名称、列类型以及通过指令添加的仪表盘卡片(例如“数量合计”)不再夹杂英文。", "模板现在用你的语言生成:用中文、印尼语或西班牙语描述,列名、下拉选项、汇总卡片和示例行都会是该语言,所有公式照常运行。", "链接可以指定语言:加上 ?lang=zh(或 en、id、es),预算、市场和法律页面就会以该语言打开,不会改变你保存的设置。", "模板工作室现在能听懂中文和西班牙语指令,例如“添加供应商列”或“añade la columna Proveedor”。", "投资页实时价格:股票和 ETF(印尼与美股)、加密货币和黄金按市场价更新。随时点“刷新价格”;你手动填写的价格会保留到你刷新为止。", "Mint 会请求浏览器保留数据,显示上次备份时间,并在预算有了足够记录后提醒你同步或备份。", - "模板和市场现在也支持中文和西班牙语,下载的 .xlsx 文件的工作表名称和提示也使用你的语言。", - "隐私政策和条款现提供印尼语、中文和西班牙语版本。" + "模板和市场现在也支持中文和西班牙语,下载的 .xlsx 文件的工作表名称和提示也使用你的语言。" ], "es": [ + "El estudio de Plantillas responde cada mensaje en tu idioma, incluidos «la columna ya existe» y los errores de fórmula, y entiende resaltados en indonesio.", "El estudio de Plantillas responde del todo en tu idioma: los nombres de tema, los tipos de columna y las tarjetas del panel que añade una orden (como «Promedio de cantidad») ya no salen en inglés.", "Las plantillas salen en tu idioma: una petición en español, chino o indonesio da nombres de columna, listas, tarjetas y filas de ejemplo en ese idioma, y todas las fórmulas siguen funcionando.", "Los enlaces pueden elegir el idioma: añade ?lang=es (o en, id, zh) y Budget, el mercado y las páginas legales se abren en ese idioma sin cambiar tu elección guardada.", "El estudio de Plantillas entiende órdenes en chino y en español, como «añade la columna Proveedor» o 添加供应商列.", "Precios en vivo en Invertir: acciones y ETF (IDX y EE. UU.), cripto y oro se actualizan con el mercado. Toca Actualizar precios cuando quieras; los precios que escribes tú se quedan hasta que actualices.", "Mint pide al navegador que conserve sus datos, muestra cuándo hiciste la última copia y te recuerda sincronizar o hacer copia cuando un presupuesto ya tiene historial.", - "Plantillas y el Marketplace ahora también hablan chino y español, y los .xlsx descargados usan tu idioma en pestañas y consejos.", - "Privacidad y Términos disponibles en indonesio, chino y español." + "Plantillas y el Marketplace ahora también hablan chino y español, y los .xlsx descargados usan tu idioma en pestañas y consejos." ] } } diff --git a/apps/mint/web/js/app.js b/apps/mint/web/js/app.js index 9fc3b377..60361d46 100644 --- a/apps/mint/web/js/app.js +++ b/apps/mint/web/js/app.js @@ -2445,7 +2445,8 @@ var chip = e.target.closest("[data-example]"); if (chip) { var form = chip.closest("form"); - form.querySelector("textarea").value = chip.dataset.example; + // the chip shows the example in the app language; use that text so the template comes out in it too + form.querySelector("textarea").value = (chip.textContent || "").trim() || chip.dataset.example; form.querySelector("textarea").focus(); } }); diff --git a/apps/mint/web/js/core.js b/apps/mint/web/js/core.js index 431af12f..eae1becf 100644 --- a/apps/mint/web/js/core.js +++ b/apps/mint/web/js/core.js @@ -25,8 +25,8 @@ lavender: ["lavender", "purple", "ungu", "violet", "紫", "薰衣草", "morado", "lavanda"], blush: ["blush", "pink", "pastel", "cute", "aesthetic", "girly", "粉", "rosa", "rosado"], forest: ["forest", "green", "hijau", "nature", "绿", "森林", "verde", "bosque"], - mono: ["mono", "minimal", "minimalist", "black and white", "clean", "simple", "简约", "极简", "黑白", "minimalista"], - midnight: ["midnight", "dark", "gelap", "night", "深色", "暗色", "午夜", "oscuro"] + mono: ["mono", "minimal", "minimalist", "black and white", "clean", "simple", "简约", "极简", "黑白", "minimalista", "monocromo", "blanco y negro", "hitam putih"], + midnight: ["midnight", "dark", "gelap", "tengah malam", "night", "深色", "暗色", "午夜", "oscuro", "medianoche"] }; // scale = rough units per 1 USD, used only to size sample numbers in generated templates. @@ -183,19 +183,19 @@ function inferType(name) { var n = name.toLowerCase(); - if (/(date|deadline|due|tanggal|start|end|day|tgl)/.test(n)) { + if (/(date|deadline|due|tanggal|start|end|day|tgl|日期|时间|截止|到期|fecha|vencimiento|día|plazo)/.test(n)) { return { type: "date" }; } - if (/(price|cost|amount|total|budget|revenue|fee|salary|harga|biaya|jumlah uang|value|income|expense|paid amount|setoran|iuran|gaji|bayar|nominal|modal|omzet|tagihan)/.test(n)) { + if (/(price|cost|amount|total|budget|revenue|fee|salary|harga|biaya|jumlah uang|value|income|expense|paid amount|setoran|iuran|gaji|bayar|nominal|modal|omzet|tagihan|金额|价格|费用|成本|收入|支出|预算|工资|存款|总价|precio|costo|coste|importe|monto|presupuesto|ingreso|gasto|salario|aporte|cuota|pago)/.test(n)) { return { type: "currency" }; } - if (/(%|percent|rate|margin|probability|persen)/.test(n)) { + if (/(%|percent|rate|margin|probability|persen|百分比|比例|利率|porcentaje|tasa)/.test(n)) { return { type: "percent" }; } - if (/(qty|quantity|count|hours|jam|stok|stock|units|score|minutes|reps|sets|kg|weight|number|nomor)/.test(n)) { + if (/(qty|quantity|count|hours|jam|stok|stock|units|score|minutes|reps|sets|kg|weight|number|nomor|rating|数量|次数|小时|分钟|库存|分数|评分|重量|cantidad|horas|minutos|unidades|puntuación|puntos|peso|número)/.test(n)) { return { type: "number" }; } - if (/(done|paid|complete|selesai|lunas|yes\/no|checked)/.test(n)) { + if (/(done|paid|complete|selesai|lunas|yes\/no|checked|已完成|已付|完成|是否|hecho|pagado|completado|listo)/.test(n)) { return { type: "check" }; } for (var key in DEFAULT_OPTIONS) { @@ -482,14 +482,15 @@ } function parseListedColumns(text) { - var m = /(?:columns?|kolom|fields?)\s*[:\-]\s*([^.\n]+)/i.exec(text); + var m = /(?:columns?|kolom|fields?|columnas?|campos?)\s*[::\-]\s*([^.。\n]+)/i.exec(text) || + /(?:列|字段)\s*[::]\s*([^。\n]+)/.exec(text); if (!m) { m = /\bwith\s+([a-z0-9 ,&/'%()-]+?(?:,| and )[a-z0-9 ,&/'%()-]+)$/i.exec(text.trim()); } if (!m) { return []; } - return m[1].split(/,|\band\b|\bdan\b|&/i).map(function (s) { + return m[1].split(/,|,|、|;|;|\band\b|\bdan\b|\by\b|&/i).map(function (s) { return s.trim().replace(/^(a|an|the)\s+/i, ""); }).filter(function (s) { return s.length > 1 && s.length < 40; @@ -1388,6 +1389,10 @@ return spec; } + function stripColumnList(text) { + return String(text || "").replace(/\s*(?:(?:with|dengan|con|和)\s*)?(?:columns?|kolom|fields?|columnas?|campos?|列|字段)\s*[::\-].*$/i, ""); + } + function generate(prompt, options) { options = options || {}; var text = String(prompt || "").trim(); @@ -1395,7 +1400,9 @@ var words = BLUEPRINT_L10N[lang] || {}; var currency = detectCurrency(text) || options.currency || "USD"; var theme = detectTheme(text); - var ranked = scoreBlueprints(text); + // "列:项目、费用" lists the user's own columns; a column called 项目 (project) must not pick a blueprint + var listedFirst = parseListedColumns(text); + var ranked = scoreBlueprints(listedFirst.length ? stripColumnList(text) || text : text); var spec; var notes = []; var quoted = /["\u201c]([^"\u201d]{3,60})["\u201d]/.exec(text); @@ -1463,7 +1470,13 @@ localizeSpec(spec, lang); if (spec.blueprint === "custom") { // "Registro de mis plantas" reads better than an English "… Tracker" title - var head = text.split(/[\uff0c,\u3002.;\uff1b]/)[0].replace(/\s+/g, " ").trim(); + var head = stripColumnList(text) + .split(/[\uff0c,\u3002.;\uff1b]/)[0] + .replace(/^(?:buat|bikin)\s+(?:template\s+)?/i, "") + .replace(/^(?:crea|haz|genera)\s+(?:una?\s+)?(?:plantilla\s+)?(?:de\s+)?/i, "") + .replace(/^(?:建|做|创建|生成)(?:一个|一张|个)?/, "") + .replace(/\s+/g, " ") + .trim(); if (head) { spec.title = clampText(head.charAt(0).toUpperCase() + head.slice(1), 70); } @@ -1596,11 +1609,15 @@ return n.replace(/\s*(?:列|栏|字段)$/, "").replace(/^(?:列|栏|字段)\s*/, "").trim() || n; } + var ZH_NUMBERS = { "一": 1, "两": 2, "二": 2, "三": 3, "四": 4, "五": 5, "六": 6, "七": 7, "八": 8, "九": 9, "十": 10 }; + var ES_NUMBERS = { un: 1, una: 1, otra: 1, dos: 2, tres: 3, cuatro: 4, cinco: 5, seis: 6, siete: 7, ocho: 8, nueve: 9, diez: 10 }; + var ID_NUMBERS = { satu: 1, se: 1, dua: 2, tiga: 3, empat: 4, lima: 5, enam: 6, tujuh: 7, delapan: 8, sembilan: 9, sepuluh: 10 }; + function zhTweak(spec, text) { var s = text.replace(/:/g, ":").replace(/[,、]/g, ",").replace(/=/g, "=").trim(); var m; - if ((m = /^(?:添加|新增|增加|加上?)\s*(\d{1,3})\s*(?:行|条)/.exec(s))) { - return "add " + m[1] + " rows"; + if ((m = /^(?:添加|新增|增加|加上?|再加)\s*(\d{1,3}|[一两二三四五六七八九十])?\s*(?:个)?\s*(?:新的?)?\s*(?:行|条)(?:数据|记录)?$/.exec(s))) { + return "add " + (ZH_NUMBERS[m[1]] || m[1] || 1) + " rows"; } if ((m = /^(?:添加|新增|增加|加上?)\s*(?:一个?)?\s*(?:列|栏|字段)\s*:?\s*(.+)$/.exec(s))) { return "add column " + tailToEnglish(m[1]); @@ -1635,6 +1652,11 @@ var zhOp = /小于|低于|少于|/.test(m[2]) ? "above" : "equal to"; return "highlight " + bareColumnName(spec, m[1]) + " " + zhOp + " " + m[3] + (m[4] ? " " + COLOUR_WORDS[m[4]] : ""); } + // column first: "Actual 小于 0 标红", "把 利润 低于 0 的标成红色" + if ((m = /^(?:把|将|当)?\s*(.+?)\s*(小于|低于|少于|<|大于|高于|多于|超过|>|等于|=)\s*(.+?)\s*(?:时|的)?\s*(?:行)?\s*(?:标为|标成|标记为|显示为|设为|变|标|高亮|突出显示)\s*(?:成|为)?\s*(红色|红|绿色|绿|黄色|黄)$/.exec(s))) { + var zhOp2 = /小于|低于|少于|/.test(m[2]) ? "above" : "equal to"; + return "highlight " + bareColumnName(spec, m[1]) + " " + zhOp2 + " " + m[3] + " " + COLOUR_WORDS[m[5] || m[4]]; + } return text; } @@ -1642,8 +1664,8 @@ var s = text.trim(); var m; var art = "(?:(?:el|la|los|las|un|una)\\s+)?"; - if ((m = /^(?:añad[ea]|añadir|agreg[ae]|agregar|inserta|insertar)\s+(\d{1,3})\s+filas?\b/i.exec(s))) { - return "add " + m[1] + " rows"; + if ((m = /^(?:añad[ea]|añadir|agreg[ae]|agregar|inserta|insertar|pon)\s+(\d{1,3}|una?|otra|dos|tres|cuatro|cinco|seis|siete|ocho|nueve|diez)?\s*(?:nuevas?\s+)?filas?(?:\s+nuevas?)?$/i.exec(s))) { + return "add " + (ES_NUMBERS[(m[1] || "").toLowerCase()] || m[1] || 1) + " rows"; } if ((m = new RegExp("^(?:añad[ea]|añadir|agreg[ae]|agregar|inserta|insertar|crea|crear)\\s+" + art + "(?:nueva\\s+)?(?:columna|campo)\\s+(.+)$", "i").exec(s))) { return "add column " + tailToEnglish(m[1]); @@ -1674,8 +1696,35 @@ return text; } + // Indonesian: the base parser knows tambah/hapus/ganti nama/tema/mata uang; this adds highlight and card words + var ID_TOTALS = { + jumlah: "sum", jumlahkan: "sum", "rata-rata": "average", ratarata: "average", + maks: "max", maksimum: "max", tertinggi: "max", minimum: "min", terendah: "min", hitung: "count" + }; + var ID_COLOURS = { merah: "red", hijau: "green", kuning: "yellow" }; + + function idTweak(text) { + var s = String(text).trim(); + var m; + if ((m = /^tambah(?:kan)?\s+(satu|se|dua|tiga|empat|lima|enam|tujuh|delapan|sembilan|sepuluh)?\s*baris(?:\s+baru)?$/i.exec(s))) { + return "add " + (ID_NUMBERS[(m[1] || "").toLowerCase()] || 1) + " rows"; + } + if ((m = /^(?:sorot|tandai|warnai)\s+(?:kolom\s+)?(.+?)\s+(?:jika\s+|kalau\s+|bila\s+)?(kurang\s+dari|di\s*bawah|lebih\s+kecil\s+dari|lebih\s+besar\s+dari|lebih\s+dari|di\s*atas|sama\s+dengan)\s+(.+?)(?:\s+(?:dengan\s+|warna\s+)?(merah|hijau|kuning))?$/i.exec(s))) { + var op = /kurang|bawah|kecil/i.test(m[2]) ? "below" : /lebih|atas/i.test(m[2]) ? "above" : "equal to"; + return "highlight " + m[1] + " " + op + " " + m[3] + (m[4] ? " " + ID_COLOURS[m[4].toLowerCase()] : ""); + } + if ((m = /^(jumlah(?:kan)?|rata-?rata|maks(?:imum)?|tertinggi|minimum|terendah|hitung)\s+(?:dari\s+|kolom\s+|of\s+)?(.+)$/i.exec(s))) { + var word = m[1].toLowerCase(); + return (ID_TOTALS[word] || ID_TOTALS[word.replace("-", "")] || "sum") + " of " + m[2]; + } + return null; + } + function localizeTweak(spec, text) { - return /[\u3400-\u9fff]/.test(text) ? zhTweak(spec, text) : esTweak(spec, text); + if (/[\u3400-\u9fff]/.test(text)) { + return zhTweak(spec, text); + } + return idTweak(text) || esTweak(spec, text); } function applyOneTweak(spec, raw) { @@ -1684,12 +1733,12 @@ var m; // add N rows - if ((m = /^(?:add|tambah(?:kan)?)\s+(\d{1,3})\s+(?:rows?|baris)/i.exec(t))) { - var n = Math.min(200, parseInt(m[1], 10)); + if ((m = /^(?:add|tambah(?:kan)?)\s+(\d{1,3}|an?|one|another)?\s*(?:new\s+|more\s+)?(?:rows?|baris)\b/i.exec(t))) { + var n = Math.min(200, /^\d+$/.test(m[1] || "") ? parseInt(m[1], 10) : 1); for (var i = 0; i < n; i++) { spec.sheet.rows.push(sampleRow(spec, spec.sheet.rows.length)); } - return "Added " + n + " rows."; + return n === 1 ? "Added a row." : "Added " + n + " rows."; } // add column NAME [as TYPE] [with options a, b] [= formula] if ((m = /^(?:add|tambah(?:kan)?)\s+(?:a\s+|an\s+)?(?:column|kolom|field)\s+(.+)$/i.exec(t))) { diff --git a/apps/mint/web/js/i18n-more.js b/apps/mint/web/js/i18n-more.js index 2e5f1428..120faf55 100644 --- a/apps/mint/web/js/i18n-more.js +++ b/apps/mint/web/js/i18n-more.js @@ -70,6 +70,7 @@ "Inventory for my online shop, green": "网店库存表,绿色", "Content calendar for TikTok and Instagram": "TikTok 和 Instagram 内容日历", "Airbnb host profit tracker": "Airbnb 房东利润追踪表", + "Buat template catatan arisan kolom: Nama, Tanggal, Setoran": "建一个互助会记录表,列:姓名、日期、存款", "Describe the template you want first": "请先描述你想要的模板", "Designed by Mint AI. Tweak anything below — or ask in words.": "由 Mint AI 设计。下面任何内容都可以修改——或直接用文字说明。", "AI returned too few columns": "AI 返回的列太少", @@ -653,6 +654,7 @@ "Inventory for my online shop, green": "Inventario para mi tienda online, verde", "Content calendar for TikTok and Instagram": "Calendario de contenido para TikTok e Instagram", "Airbnb host profit tracker": "Control de ganancias para anfitriones de Airbnb", + "Buat template catatan arisan kolom: Nama, Tanggal, Setoran": "Crea una plantilla de tanda con columnas: Nombre, Fecha y Aporte", "Describe the template you want first": "Primero describe la plantilla que quieres", "Designed by Mint AI. Tweak anything below — or ask in words.": "Diseñada por Mint AI. Ajusta lo que quieras abajo, o pídelo con palabras.", "AI returned too few columns": "La IA devolvió muy pocas columnas", @@ -1189,6 +1191,19 @@ } }; + var FORMULA_WHY = { + zh: { unknown: "未知列 [{name}]", self: "列不能引用自身" }, + es: { unknown: "columna desconocida [{name}]", self: "una columna no puede hacer referencia a sí misma" } + }; + + function formulaWhy(lang, why) { + var unknown = /^unknown column \[(.+)\]$/.exec(why); + if (unknown) { + return FORMULA_WHY[lang].unknown.replace("{name}", unknown[1]); + } + return why === "a column cannot reference itself" ? FORMULA_WHY[lang].self : why; + } + function studioWord(lang, word) { var map = STUDIO_WORDS[lang]; return Object.prototype.hasOwnProperty.call(map, word) ? map[word] : word; @@ -1226,12 +1241,19 @@ [/^Added column “(.+)” \((\w+)\)\.$/, function (m, name, type) { return "已添加列“" + name + "”(" + studioWord("zh", type) + ")。"; }], [/^Added a “(.+)” column\.$/, "已添加“$1”列。"], [/^Added (\d+) rows\.$/, "已添加 $1 行。"], + [/^Added a row\.$/, "已添加 1 行。"], [/^Removed “(.+)”\.$/, "已删除“$1”。"], [/^Renamed the template to “(.+)”\.$/, "模板已重命名为“$1”。"], [/^Renamed “(.+)” to “(.+)”\.$/, "已将“$1”重命名为“$2”。"], [/^Currency changed to (.+)\.$/, "货币已改为 $1。"], [/^Theme set to (.+)\.$/, function (m, theme) { return "主题已设为" + studioWord("zh", theme) + "。"; }], + [/^Earn (\d+)%$/, "赚 $1%"], [/^Theme: (.+)\.$/, function (m, theme) { return "主题:" + studioWord("zh", theme) + "。"; }], + [/^Column “(.+)” already exists\.$/, "列“$1”已存在。"], + [/^No column named “(.+)”\.$/, "没有名为“$1”的列。"], + [/^Column names can’t be empty\.$/, "列名不能为空。"], + [/^Another column is already called “(.+)”\.$/, "已有其他列叫“$1”。"], + [/^Formula error: (.+)$/, function (m, why) { return "公式错误:" + formulaWhy("zh", why) + "。"; }], [/^Currency: (\w+)\.$/, "货币:$1。"], [/^AI unavailable \((.+)\) — used the quick generator\.$/, "AI 暂不可用($1)——已改用快速生成器。"], [/^Generated with your AI model \((.+)\)\.$/, "由你的 AI 模型($1)生成。"], @@ -1271,12 +1293,19 @@ [/^Added column “(.+)” \((\w+)\)\.$/, function (m, name, type) { return "Columna “" + name + "” (" + studioWord("es", type) + ") añadida."; }], [/^Added a “(.+)” column\.$/, "Columna “$1” añadida."], [/^Added (\d+) rows\.$/, "$1 filas añadidas."], + [/^Added a row\.$/, "Fila añadida."], [/^Removed “(.+)”\.$/, "“$1” eliminada."], [/^Renamed the template to “(.+)”\.$/, "Plantilla renombrada a “$1”."], [/^Renamed “(.+)” to “(.+)”\.$/, "“$1” renombrada a “$2”."], [/^Currency changed to (.+)\.$/, "Moneda cambiada a $1."], [/^Theme set to (.+)\.$/, function (m, theme) { return "Tema cambiado a " + studioWord("es", theme) + "."; }], + [/^Earn (\d+)%$/, "Gana $1%"], [/^Theme: (.+)\.$/, function (m, theme) { return "Tema: " + studioWord("es", theme) + "."; }], + [/^Column “(.+)” already exists\.$/, "La columna “$1” ya existe."], + [/^No column named “(.+)”\.$/, "No hay ninguna columna llamada “$1”."], + [/^Column names can’t be empty\.$/, "El nombre de la columna no puede estar vacío."], + [/^Another column is already called “(.+)”\.$/, "Ya hay otra columna llamada “$1”."], + [/^Formula error: (.+)$/, function (m, why) { return "Error en la fórmula: " + formulaWhy("es", why) + "."; }], [/^Currency: (\w+)\.$/, "Moneda: $1."], [/^AI unavailable \((.+)\) — used the quick generator\.$/, "IA no disponible ($1): usé el generador rápido."], [/^Generated with your AI model \((.+)\)\.$/, "Generado con tu modelo de IA ($1)."], diff --git a/apps/mint/web/js/i18n.js b/apps/mint/web/js/i18n.js index 7f77ab1d..da60e145 100644 --- a/apps/mint/web/js/i18n.js +++ b/apps/mint/web/js/i18n.js @@ -76,7 +76,7 @@ "← All guides": "← Semua panduan", "Earn without making templates": "Dapat penghasilan tanpa membuat template", "Join the affiliate program: share any listing with an “Earn %” badge and get paid commission on every sale you refer.": - "Ikut program afiliasi: bagikan listing dengan lencana “Earn %” dan dapatkan komisi dari setiap penjualan yang kamu bawa.", + "Ikut program afiliasi: bagikan listing dengan lencana “Komisi %” dan dapatkan komisi dari setiap penjualan yang kamu bawa.", "How it works": "Cara kerjanya", "How it works →": "Cara kerjanya →", "No listings yet.": "Belum ada listing.", @@ -172,7 +172,7 @@ "Free": "Gratis", "New": "Baru", "Earn": "Dapat", - "Earn %": "Earn %", + "Earn %": "Komisi %", "No ratings yet": "Belum ada rating", // item page @@ -348,7 +348,7 @@ // affiliate "Affiliate program": "Program afiliasi", - "Log in, open any listing with an “Earn %” badge and get your link.": "Masuk, buka listing dengan lencana “Earn %”, lalu dapatkan link-mu.", + "Log in, open any listing with an “Earn %” badge and get your link.": "Masuk, buka listing dengan lencana “Komisi %”, lalu dapatkan link-mu.", "Affiliate dashboard": "Dashboard afiliasi", "Where should sellers pay you?": "Penjual harus membayarmu ke mana?", "Shown only to sellers who owe you commission. E.g. “BCA 1234567 a.n. Your Name”, “GoPay 0812…” or a PayPal email.": @@ -678,6 +678,14 @@ SUM: "JUMLAH", AVERAGE: "RATA-RATA", MAX: "MAKS", MIN: "MIN", COUNTA: "HITUNG" }; + function formulaWhyId(why) { + var unknown = /^unknown column \[(.+)\]$/.exec(why); + if (unknown) { + return "kolom [" + unknown[1] + "] tidak ada"; + } + return why === "a column cannot reference itself" ? "kolom tidak boleh merujuk dirinya sendiri" : why; + } + function studioWord(word) { return Object.prototype.hasOwnProperty.call(STUDIO_WORDS_ID, word) ? STUDIO_WORDS_ID[word] : word; } @@ -714,18 +722,25 @@ [/^Added column “(.+)” \((\w+)\)\.$/, function (m, name, type) { return "Kolom “" + name + "” (" + studioWord(type) + ") ditambahkan."; }], [/^Added a “(.+)” column\.$/, "Kolom “$1” ditambahkan."], [/^Added (\d+) rows\.$/, "$1 baris ditambahkan."], + [/^Added a row\.$/, "1 baris ditambahkan."], [/^Removed “(.+)”\.$/, "“$1” dihapus."], [/^Renamed the template to “(.+)”\.$/, "Template diganti nama menjadi “$1”."], [/^Renamed “(.+)” to “(.+)”\.$/, "“$1” diganti nama menjadi “$2”."], [/^Currency changed to (.+)\.$/, "Mata uang diganti ke $1."], [/^Theme set to (.+)\.$/, function (m, theme) { return "Tema diatur ke " + studioWord(theme) + "."; }], + [/^Earn (\d+)%$/, "Komisi $1%"], [/^Theme: (.+)\.$/, function (m, theme) { return "Tema: " + studioWord(theme) + "."; }], + [/^Column “(.+)” already exists\.$/, "Kolom “$1” sudah ada."], + [/^No column named “(.+)”\.$/, "Tidak ada kolom bernama “$1”."], + [/^Column names can’t be empty\.$/, "Nama kolom tidak boleh kosong."], + [/^Another column is already called “(.+)”\.$/, "Sudah ada kolom lain bernama “$1”."], + [/^Formula error: (.+)$/, function (m, why) { return "Rumus salah: " + formulaWhyId(why) + "."; }], [/^Currency: (\w+)\.$/, "Mata uang: $1."], [/^AI unavailable \((.+)\) — used the quick generator\.$/, "AI tidak tersedia ($1) — memakai generator cepat."], [/^Generated with your AI model \((.+)\)\.$/, "Dibuat dengan model AI-mu ($1)."], [/^Added a (\w+) card for “(.+)”\.$/, function (m, fn, name) { return "Kartu " + studioWord(fn) + " untuk “" + name + "” ditambahkan."; }], [/^Highlight rule added on “(.+)”\.$/, "Aturan sorotan ditambahkan pada “$1”."], - [/^Didn’t understand “(.+)”\. Try: (.+)$/, "Tidak paham “$1”. Coba: $2"], + [/^Didn’t understand “(.+)”\. Try: (.+)$/, "Tidak paham “$1”. Coba: tambah kolom Prioritas · hapus kolom Catatan · ganti nama Biaya jadi Harga · tambah 5 baris · tema ocean · mata uang IDR · total Jumlah · sorot Laba kurang dari 0 merah."], [/^(.+) connected — orders now unlock automatically$/, "$1 terhubung — pesanan kini terbuka otomatis"] ]; diff --git a/apps/mint/web/sw.js b/apps/mint/web/sw.js index f9490d35..0b5960b7 100644 --- a/apps/mint/web/sw.js +++ b/apps/mint/web/sw.js @@ -1,6 +1,6 @@ /* Mint Templates service worker — app shell cache. API calls (api/...) always go to the network. * Mint Budget (budget/) has its own worker with the narrower scope budget/, so it is not cached here. */ -var CACHE = "mint-v3.5.3"; +var CACHE = "mint-v3.5.4"; var SHELL = [ "./", "index.html", "app.html", "web.html", "mobile.html", "privacy.html", "terms.html", "delete-account.html", "app.css", "js/locale.js", "js/mint-cloud.js", "js/core.js", "js/xlsx.js", "js/guides.js", "js/store.js", "js/i18n-more.js", "js/i18n.js", "js/app.js",