Find Missing Translations Overview Extract string resource keys from the default that are absent in a target locale's , excluding non-translatable entries. Outputs missing keys and offers to translate them. When to Use - Need to find untranslated strings for a specific locale - Preparing a batch of strings for a translator - Checking translation coverage after adding new features Background: Crowdin strip-identical behavior This repo syncs translations via Crowdin (branch ). Crowdin's default export behavior omits any translation that exactly equals the source , so a key that the translator d…

2>/dev/null)\nif [ -z \"$sync_ts\" ]; then\n echo \"WARNING: no Crowdin export commit found in history; treating all missing as actionable\" >&2\n sync_ts=0\nelse\n echo \"Crowdin sync cutoff: $(git log -1 --format='%ci %h' --grep='^New Crowdin translations by GitHub Action

Find Missing Translations Overview Extract string resource keys from the default that are absent in a target locale's , excluding non-translatable entries. Outputs missing keys and offers to translate them. When to Use - Need to find untranslated strings for a specific locale - Preparing a batch of strings for a translator - Checking translation coverage after adding new features Background: Crowdin strip-identical behavior This repo syncs translations via Crowdin (branch ). Crowdin's default export behavior omits any translation that exactly equals the source , so a key that the translator d…

)\"\nfi\n\n# For each locale, list only keys added after the Crowdin sync (truly new).\n# Run for BOTH \u003cstring> and \u003cplurals>.\nfor locale in cs-rCZ de-rDE sv-rSE; do\n echo \"=== $locale: genuinely new (post-sync) \u003cstring> keys ===\"\n comm -23 \\\n \u003c(grep '\u003cstring name=' amethyst/src/main/res/values/strings.xml \\\n | grep -v 'translatable=\"false\"' \\\n | sed 's/.*name=\"\\([^\"]*\\)\".*/\\1/' | sort) \\\n \u003c(grep '\u003cstring name=' amethyst/src/main/res/values-$locale/strings.xml \\\n | sed 's/.*name=\"\\([^\"]*\\)\".*/\\1/' | sort) \\\n | while IFS= read -r key; do\n added_ts=$(git log -1 --format=%ct -S \"name=\\\"$key\\\"\" -- amethyst/src/main/res/values/strings.xml)\n if [ -n \"$added_ts\" ] && [ \"$added_ts\" -gt \"$sync_ts\" ]; then\n echo \"$key\"\n fi\n done\n\n echo \"=== $locale: genuinely new (post-sync) \u003cplurals> keys ===\"\n comm -23 \\\n \u003c(grep '\u003cplurals name=' amethyst/src/main/res/values/strings.xml \\\n | sed 's/.*name=\"\\([^\"]*\\)\".*/\\1/' | sort) \\\n \u003c(grep '\u003cplurals name=' amethyst/src/main/res/values-$locale/strings.xml \\\n | sed 's/.*name=\"\\([^\"]*\\)\".*/\\1/' | sort) \\\n | while IFS= read -r key; do\n added_ts=$(git log -1 --format=%ct -S \"name=\\\"$key\\\"\" -- amethyst/src/main/res/values/strings.xml)\n if [ -n \"$added_ts\" ] && [ \"$added_ts\" -gt \"$sync_ts\" ]; then\n echo \"$key\"\n fi\n done\ndone\n```\n\n**Why this beats using the `l10n_crowdin_translations` branch tip:**\n- The branch is often deleted after merge — the branch tip then doesn't exist or points to a stale ref.\n- The branch tip may include Crowdin commits that haven't been merged to main yet. Those changes aren't in our working tree, so they don't affect what's on disk for us. The \"reachable from HEAD\" cutoff matches what we can actually observe in `values-*/strings.xml`.\n\n**Only the listed (post-sync) keys are actionable.** Anything older is either:\n- A deliberate \"use English\" choice in Crowdin (brand terms like `Nowhere X`, loanwords like `Apps` / `Feed` / `Issues`, version prefixes like `v%1$s`), or\n- A pending translation the translator hasn't filled in yet — still Crowdin's job, not ours.\n\nIn both cases, Android's resource resolution falls back to `values/strings.xml` at runtime, so there is no user-visible bug. Adding source-identical fallbacks locally is noise that the next sync will strip again.\n\nReport the pre-sync skipped count as a one-liner (\"N keys predate the last Crowdin sync, skipped — Crowdin owns them\"). Do not list them or propose translations.\n\nIf no Crowdin export commit can be found in history (`sync_ts=0` fallback), warn the user and fall back to treating all missing keys as actionable — but flag that the workflow is degraded.\n\n### 3. Get English values for missing keys\n\nFor each missing key, extract its English value. `\u003cstring>` is a single line; `\u003cplurals>` is a multi-line block — handle each appropriately.\n\n```bash\n# Missing \u003cstring>: full line from default strings.xml\nwhile IFS= read -r key; do\n grep \"name=\\\"$key\\\"\" amethyst/src/main/res/values/strings.xml\ndone \u003c \u003c(comm -23 \\\n \u003c(grep '\u003cstring name=' amethyst/src/main/res/values/strings.xml \\\n | grep -v 'translatable=\"false\"' \\\n | sed 's/.*name=\"\\([^\"]*\\)\".*/\\1/' | sort) \\\n \u003c(grep '\u003cstring name=' amethyst/src/main/res/values-cs-rCZ/strings.xml \\\n | sed 's/.*name=\"\\([^\"]*\\)\".*/\\1/' | sort))\n\n# Missing \u003cplurals>: extract the multi-line block (opening tag through \u003c/plurals>)\nwhile IFS= read -r key; do\n awk -v key=\"$key\" '\n $0 ~ \"\u003cplurals name=\\\"\" key \"\\\"\" { in_p = 1 }\n in_p { print }\n in_p && /\u003c\\/plurals>/ { in_p = 0 }\n ' amethyst/src/main/res/values/strings.xml\ndone \u003c \u003c(comm -23 \\\n \u003c(grep '\u003cplurals name=' amethyst/src/main/res/values/strings.xml \\\n | sed 's/.*name=\"\\([^\"]*\\)\".*/\\1/' | sort) \\\n \u003c(grep '\u003cplurals name=' amethyst/src/main/res/values-cs-rCZ/strings.xml \\\n | sed 's/.*name=\"\\([^\"]*\\)\".*/\\1/' | sort))\n```\n\n### 4. Audit missing strings for plural-shaped patterns\n\nBefore presenting results, **scan the missing English strings** for two red-flag patterns and warn the user about each match:\n\n1. **Hardcoded `\"1\"` next to a noun.** A new English string like `\"1 reply\"`, `\"1 follower\"`, or `\"1 minute ago\"` almost always belongs in a `\u003cplurals>` resource — not a `\u003cstring>`. Hardcoding `1` in English forces every translator to either also hardcode `1` (breaking languages where the `one` category covers other numbers, e.g. some Slavic languages) or to silently change the meaning.\n2. **A `%d` / `%1$d` placeholder in a clearly singular/plural sentence** (e.g. `\"%1$d reply\"`, `\"%d follower\"`). Even though the placeholder is parameterised, English-only `one`/`other` agreement won't survive translation into languages that need `few`/`many`.\n\nAlso **audit existing `\u003cplurals>` resources** for two anti-patterns:\n\n1. **`quantity=\"one\"` items that hardcode the literal `1`** (instead of using a `%d` / `%1$d` placeholder) — broken for languages where the `one` CLDR category covers more than just `n=1` (Russian, Ukrainian, Croatian, etc.).\n2. **`quantity=\"zero\"` items in any locale that doesn't natively use the `zero` CLDR category** — i.e. **everything except Arabic (`ar`) and Welsh (`cy`)**. ICU/CLDR maps `count=0` to `other` for English and all the locales we ship to (cs, de, pt-BR, sv, etc.), so `\u003citem quantity=\"zero\">` is **dead code** there: `getQuantityString(id, 0)` will pick `other`, never the zero entry, and the visible runtime string ends up `\"…0 items\"` instead of the intended `\"…no items\"`.\n\nIf a UX genuinely wants special \"no items\" wording at count=0, that has to be a call-site `if (count == 0)` branch to a separate `\u003cstring>`, **not** a `quantity=\"zero\"` plural item.\n\nFlag and offer to fix:\n\n```bash\n# Scan every locale's strings.xml for \u003citem quantity=\"one\"> entries that\n# hardcode \"1\" (or other literal digits) instead of using a placeholder.\n# Looks at default + all values-* locales.\nfor f in amethyst/src/main/res/values/strings.xml amethyst/src/main/res/values-*/strings.xml; do\n awk -v file=\"$f\" '\n /\u003cplurals/ { in_plurals = 1; name = $0; sub(/.*name=\"/, \"\", name); sub(/\".*/, \"\", name) }\n in_plurals && /quantity=\"one\"/ {\n # Extract item text (between > and \u003c)\n text = $0; sub(/^[^>]*>/, \"\", text); sub(/\u003c.*$/, \"\", text)\n # Flag if it contains a digit AND no %d / %1$d placeholder\n if (text ~ /[0-9]/ && text !~ /%[0-9]*\\$?d/) {\n print file \": \u003cplurals name=\\\"\" name \"\\\"> one=\\\"\" text \"\\\"\"\n }\n }\n /\u003c\\/plurals>/ { in_plurals = 0 }\n ' \"$f\"\ndone\n```\n\nThen scan for dead `quantity=\"zero\"` entries. CLDR's `zero` category is integer-bearing only in **Arabic (`ar`)** and **Welsh (`cy`)**. In every other locale, count=0 falls through to `other`, so a `\u003citem quantity=\"zero\">` entry is dead and likely a translator/author bug (or it silently never fires):\n\n```bash\nfor f in amethyst/src/main/res/values/strings.xml amethyst/src/main/res/values-*/strings.xml; do\n # Skip Arabic and Welsh — they natively use the zero category.\n case \"$f\" in\n *values-ar*|*values-cy*) continue ;;\n esac\n awk -v file=\"$f\" '\n /\u003cplurals/ { in_plurals = 1; name = $0; sub(/.*name=\"/, \"\", name); sub(/\".*/, \"\", name) }\n in_plurals && /quantity=\"zero\"/ {\n text = $0; sub(/^[^>]*>/, \"\", text); sub(/\u003c.*$/, \"\", text)\n print file \": \u003cplurals name=\\\"\" name \"\\\"> zero=\\\"\" text \"\\\"\"\n }\n /\u003c\\/plurals>/ { in_plurals = 0 }\n ' \"$f\"\ndone\n```\n\nFor each hit, warn the user that the entry is unreachable in that locale. The fix is to **remove the `\u003citem quantity=\"zero\">`** and, if the UX wanted distinct wording for count=0, add a separate `\u003cstring>` plus an `if (count == 0)` branch at the call site (see \"Plurals: handle with care\" below).\n\nQuick scan over the missing keys:\n\n```bash\n# Flag missing English values that look like they should be \u003cplurals>\nwhile IFS= read -r key; do\n line=$(grep \"name=\\\"$key\\\"\" amethyst/src/main/res/values/strings.xml)\n # Hardcoded standalone \"1\" (word-boundary), or a count placeholder followed by a likely-countable noun\n if echo \"$line\" | grep -qE '>([^\u003c]*\\b1\\b[^\u003c]*|[^\u003c]*%[0-9]*\\$?d[^\u003c]*)\u003c'; then\n echo \"PLURAL CANDIDATE: $line\"\n fi\ndone \u003c \u003c(comm -23 \\\n \u003c(grep '\u003cstring name=' amethyst/src/main/res/values/strings.xml \\\n | grep -v 'translatable=\"false\"' \\\n | sed 's/.*name=\"\\([^\"]*\\)\".*/\\1/' | sort) \\\n \u003c(grep '\u003cstring name=' amethyst/src/main/res/values-cs-rCZ/strings.xml \\\n | sed 's/.*name=\"\\([^\"]*\\)\".*/\\1/' | sort))\n```\n\nThe regex is intentionally noisy — review each hit by hand. Many `%d` strings (e.g. `\"Limits for kind %1$d\"`, `\"Max event size (bytes)\"`) are *not* plural-bearing. Only flag the ones whose surrounding noun changes form with the count.\n\nFor each genuine match, **stop and warn the user before translating**, e.g.:\n\n> ⚠️ `notification_count` is `\"1 new reply\"` — this hardcodes `\"1\"` and should likely be a `\u003cplurals>` resource (e.g. `quantity=\"one\"` → `\"%d new reply\"`, `quantity=\"other\"` → `\"%d new replies\"`). Convert before translating?\n\nDo not silently translate plural-shaped `\u003cstring>` entries; the wrong shape will then need to be fixed in every locale.\n\n### 5. Present results and ask to translate\n\nOutput the missing entries as raw XML resource lines (copy-paste ready):\n\n```xml\n \u003cstring name=\"attestation_valid\">Valid\u003c/string>\n \u003cstring name=\"attestation_valid_from\">Valid from %1$s\u003c/string>\n \u003cstring name=\"feed_group_lists\">Lists\u003c/string>\n```\n\nAlso check `\u003cstring-array>` and `\u003cplurals>` tags using the same approach if the project uses them.\n\n#### Plurals: handle with care\n\nWhen adding or proposing **`\u003cplurals>`** entries, follow these rules:\n\n- **Never hardcode `\"1\"`** in the English text of a `quantity=\"one\"` item. Use the format placeholder (e.g. `%1$d` / `%d`) so the runtime substitutes the actual count. Hardcoding `\"1\"` breaks every language whose `one` category covers numbers other than 1 (e.g. some Slavic languages).\n- **Don't assume `one` + `other` is enough.** CLDR plural categories vary by language: `zero`, `one`, `two`, `few`, `many`, `other`. Always include **every category the target language uses**, not just the categories present in English. Examples:\n - English (`en`): `one`, `other`\n - Czech (`cs`): `one`, `few`, `many`, `other`\n - Polish (`pl`): `one`, `few`, `many`, `other`\n - Russian (`ru`): `one`, `few`, `many`, `other`\n - Arabic (`ar`): `zero`, `one`, `two`, `few`, `many`, `other`\n - German / Swedish / Brazilian Portuguese: `one`, `other`\n- When a missing string contains a count placeholder and is conceptually a singular/plural pair, **flag it before translating** — it may belong as a `\u003cplurals>` resource rather than a single `\u003cstring>`. Surface this to the user before proposing translations.\n- **Do not use `quantity=\"zero\"` outside Arabic (`ar`) and Welsh (`cy`).** CLDR's `zero` category is integer-bearing only in those two languages. Android calls `PluralRules.select(0)` for the device locale; in English/German/Czech/Polish/Russian/Swedish/Portuguese/etc. it returns `other`, so the explicit `\u003citem quantity=\"zero\">` is never picked at runtime and the user sees `\"…0 items\"` instead of the intended wording. If the design calls for \"no items\" at count=0, model it as a separate `\u003cstring>` and an `if (count == 0)` branch at the call site:\n ```kotlin\n val label = if (count == 0) {\n stringRes(R.string.foo_no_items, dateLabel)\n } else {\n pluralStringResource(R.plurals.foo_items, count, dateLabel, count)\n }\n ```\n- Reference: [Android `\u003cplurals>` docs](https://developer.android.com/guide/topics/resources/string-resource#Plurals) and [CLDR plural rules](https://unicode-org.github.io/cldr-staging/charts/latest/supplemental/language_plural_rules.html).\n\n**Then ask the user:** \"Would you like me to translate these missing strings into [list of target locales]?\"\n\n### 6. Adding translations (if approved)\n\nWhen adding translated strings to locale files:\n\n- **Append new strings at the bottom** of the file, just before the closing `\u003c/resources>` tag.\n- Do NOT try to insert them in alphabetical or matching order — a separate process handles ordering.\n\n## Common Mistakes\n\n- **Forgetting `translatable=\"false\"`** — these should never appear in locale files\n- **Diffing only `\u003cstring name=`** — `\u003cplurals>` is a separate resource type; a source `\u003cplurals>` missing from a locale will never show up in a `\u003cstring>` diff. Always run the diff twice (once per resource type) as shown in Step 2. The same goes for `\u003cstring-array>` if the project uses it.\n- **Treating every missing key as actionable** — Crowdin strips on export any translation the translator marked as \"use English\", and we cannot distinguish that from \"never seen\" by looking at disk. Use the Step 2.5 sync-timestamp filter: only keys added to `values/strings.xml` after the last `l10n_crowdin_translations` sync are genuinely new.\n- **Trying to detect \"stripped\" from git history alone** — the on-disk locale file only sees keys the translator typed a non-identical value for. The \"translator opened the key and picked English from the start\" case never touches disk, so a history-only check misses it. Use the sync-timestamp cutoff instead.\n- **Adding source-identical fallbacks locally** — they get overwritten on the next Crowdin sync. Android falls back to `values/strings.xml` at runtime anyway, so there is no user-visible bug to fix.\n- **Skipping per-locale diffs when only diffing cs-rCZ** — Crowdin can strip different keys in different locales (each translator's choice), so cs-rCZ is not a reliable upper bound. Diff each target locale, then apply the sync-timestamp filter.\n- **Inserting strings in a specific position** — always append at the bottom; ordering is handled separately\n- **Hardcoding `\"1\"` in a `\u003cplurals>` `quantity=\"one\"` item** — always use the count placeholder; otherwise non-English `one` categories produce wrong text\n- **Copying English's `one`/`other` set into every locale** — each language must include all CLDR plural categories it uses (e.g. Czech needs `one`, `few`, `many`, `other`)\n- **Using `\u003citem quantity=\"zero\">` to special-case count=0** — outside Arabic and Welsh, this entry is unreachable: ICU/CLDR maps 0 → `other`, so the runtime never picks the zero item and the user sees `\"…0 items\"`. Special-case at the call site with a separate `\u003cstring>` instead.\n---","attachment_filenames":[],"attachments":[],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"Find Missing Translations","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Overview","type":"text"}]},{"type":"paragraph","content":[{"text":"Extract string resource keys from the default ","type":"text"},{"text":"values/strings.xml","type":"text","marks":[{"type":"code_inline"}]},{"text":" that are absent in a target locale's ","type":"text"},{"text":"strings.xml","type":"text","marks":[{"type":"code_inline"}]},{"text":", excluding non-translatable entries. Outputs missing keys and offers to translate them.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"When to Use","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Need to find untranslated strings for a specific locale","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Preparing a batch of strings for a translator","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Checking translation coverage after adding new features","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Background: Crowdin strip-identical behavior","type":"text"}]},{"type":"paragraph","content":[{"text":"This repo syncs translations via Crowdin (branch ","type":"text"},{"text":"l10n_crowdin_translations","type":"text","marks":[{"type":"code_inline"}]},{"text":"). Crowdin's default export behavior ","type":"text"},{"text":"omits any translation that exactly equals the source","type":"text","marks":[{"type":"strong"}]},{"text":", so a key that the translator deliberately kept as English (common for brand terms like ","type":"text"},{"text":"\"Nowhere Drop\"","type":"text","marks":[{"type":"code_inline"}]},{"text":", single-word loanwords like ","type":"text"},{"text":"\"Apps\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"\"Feed\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"\"Issues\"","type":"text","marks":[{"type":"code_inline"}]},{"text":", or version prefixes like ","type":"text"},{"text":"\"v%1$s\"","type":"text","marks":[{"type":"code_inline"}]},{"text":") will not appear in the locale's ","type":"text"},{"text":"strings.xml","type":"text","marks":[{"type":"code_inline"}]},{"text":" even though the Crowdin UI shows it as 100% translated.","type":"text"}]},{"type":"paragraph","content":[{"text":"Consequences for this skill:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"A \"missing\" key on disk is not always actionable.","type":"text","marks":[{"type":"strong"}]},{"text":" It may be Crowdin-stripped (translator already chose source-identical and Crowdin didn't export it) rather than genuinely new.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Don't add source-identical fallbacks locally.","type":"text","marks":[{"type":"strong"}]},{"text":" Android's resource resolution falls back to ","type":"text"},{"text":"values/strings.xml","type":"text","marks":[{"type":"code_inline"}]},{"text":" at runtime, so the user already sees the correct text. Local additions will be silently overwritten on Crowdin's next sync anyway.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"The only actionable cases are keys Crowdin has never exported.","type":"text","marks":[{"type":"strong"}]},{"text":" Whether the translator picked \"use English\" or simply hasn't translated the key yet, both states are owned by Crowdin and look identical on disk. The local repo cannot distinguish them.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"The Step 2.5 filter below uses the ","type":"text"},{"text":"most recent Crowdin export commit reachable from ","type":"text","marks":[{"type":"strong"}]},{"text":"HEAD","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" (subject: ","type":"text"},{"text":"\"New Crowdin translations by GitHub Action\"","type":"text","marks":[{"type":"code_inline"}]},{"text":") as the cutoff: any key added to ","type":"text"},{"text":"values/strings.xml","type":"text","marks":[{"type":"code_inline"}]},{"text":" after that commit is genuinely new (Crowdin hasn't exported it yet); anything older is Crowdin's responsibility regardless of why it's missing. The reachable-from-HEAD check survives the common workflow of deleting the ","type":"text"},{"text":"l10n_crowdin_translations","type":"text","marks":[{"type":"code_inline"}]},{"text":" branch after merging.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Target Locales","type":"text"}]},{"type":"paragraph","content":[{"text":"The default set of locales (unless the user specifies otherwise):","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Locale","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Language","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Directory","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"cs-rCZ","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Czech","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"values-cs-rCZ","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"pt-rBR","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Brazilian Portuguese","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"values-pt-rBR","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"sv-rSE","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Swedish","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"values-sv-rSE","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"de-rDE","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"German","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"values-de-rDE","type":"text","marks":[{"type":"code_inline"}]}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Technique","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"1. Identify files","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"Default: amethyst/src/main/res/values/strings.xml\nTarget: amethyst/src/main/res/values-\u003clocale>/strings.xml","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"2. Find missing keys using cs-rCZ as reference","type":"text"}]},{"type":"paragraph","content":[{"text":"Always diff against ","type":"text"},{"text":"cs-rCZ","type":"text","marks":[{"type":"code_inline"}]},{"text":" first — it is the most complete locale and serves as the reference. Any keys missing in ","type":"text"},{"text":"cs-rCZ","type":"text","marks":[{"type":"code_inline"}]},{"text":" will also be missing in the other target locales.","type":"text"}]},{"type":"paragraph","content":[{"text":"You MUST diff ","type":"text"},{"text":"both","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"\u003cstring name=","type":"text","marks":[{"type":"code_inline"}]},{"text":" AND ","type":"text"},{"text":"\u003cplurals name=","type":"text","marks":[{"type":"code_inline"}]},{"text":" — these are independent resource types and a key that is a ","type":"text"},{"text":"\u003cplurals>","type":"text","marks":[{"type":"code_inline"}]},{"text":" in the source will never appear in a ","type":"text"},{"text":"\u003cstring>","type":"text","marks":[{"type":"code_inline"}]},{"text":" diff. Forgetting ","type":"text"},{"text":"\u003cplurals>","type":"text","marks":[{"type":"code_inline"}]},{"text":" is the most common silent failure of this skill (it misses things like ","type":"text"},{"text":"music_playlist_track_count","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"notification_count_more","type":"text","marks":[{"type":"code_inline"}]},{"text":", etc.).","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Strings: extract translatable keys from default (exclude translatable=\"false\")\necho \"=== missing \u003cstring> ===\"\ncomm -23 \\\n \u003c(grep '\u003cstring name=' amethyst/src/main/res/values/strings.xml \\\n | grep -v 'translatable=\"false\"' \\\n | sed 's/.*name=\"\\([^\"]*\\)\".*/\\1/' | sort) \\\n \u003c(grep '\u003cstring name=' amethyst/src/main/res/values-cs-rCZ/strings.xml \\\n | sed 's/.*name=\"\\([^\"]*\\)\".*/\\1/' | sort)\n\n# Plurals: a separate resource type — MUST be diffed independently\necho \"=== missing \u003cplurals> ===\"\ncomm -23 \\\n \u003c(grep '\u003cplurals name=' amethyst/src/main/res/values/strings.xml \\\n | sed 's/.*name=\"\\([^\"]*\\)\".*/\\1/' | sort) \\\n \u003c(grep '\u003cplurals name=' amethyst/src/main/res/values-cs-rCZ/strings.xml \\\n | sed 's/.*name=\"\\([^\"]*\\)\".*/\\1/' | sort)","type":"text"}]},{"type":"paragraph","content":[{"text":"This gives two lists of missing key names — keep them separate; ","type":"text"},{"text":"\u003cplurals>","type":"text","marks":[{"type":"code_inline"}]},{"text":" translations need the per-locale CLDR category set (see Step 5 → \"Plurals: handle with care\"). Do NOT diff each locale separately for strings — assume the same keys are missing in all target locales (but DO repeat the ","type":"text"},{"text":"\u003cplurals>","type":"text","marks":[{"type":"code_inline"}]},{"text":" diff per locale if you suspect Crowdin asymmetric stripping).","type":"text"}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"Caveat:","type":"text","marks":[{"type":"strong"}]},{"text":" Crowdin can asymmetrically strip keys across locales (each translator independently chose source-identical for different keys). If the cs-rCZ list looks suspiciously short, run the same diff for each target locale individually and union the results before Step 2.5.","type":"text"}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"2.5. Filter out keys Crowdin has already seen (sync-timestamp check)","type":"text"}]},{"type":"paragraph","content":[{"text":"A missing key is ","type":"text"},{"text":"only actionable if Crowdin has never exported it","type":"text","marks":[{"type":"strong"}]},{"text":". Once a key has been pushed to Crowdin and exported back, the translator may have chosen \"use English\" — Crowdin stores that choice in its own database and strips the entry from the exported ","type":"text"},{"text":"strings.xml","type":"text","marks":[{"type":"code_inline"}]},{"text":". From disk we cannot tell \"translator picked English\" from \"Crowdin never saw the key\": both look identical.","type":"text"}]},{"type":"paragraph","content":[{"text":"The reliable signal is ","type":"text"},{"text":"time","type":"text","marks":[{"type":"strong"}]},{"text":": compare when the key was added to ","type":"text"},{"text":"values/strings.xml","type":"text","marks":[{"type":"code_inline"}]},{"text":" against the timestamp of the ","type":"text"},{"text":"most recent Crowdin export that has been merged into the current branch","type":"text","marks":[{"type":"strong"}]},{"text":". Crowdin's GitHub Action produces commits with the literal subject ","type":"text"},{"text":"New Crowdin translations by GitHub Action","type":"text","marks":[{"type":"code_inline"}]},{"text":"; finding the latest such commit reachable from ","type":"text"},{"text":"HEAD","type":"text","marks":[{"type":"code_inline"}]},{"text":" works even if the ","type":"text"},{"text":"l10n_crowdin_translations","type":"text","marks":[{"type":"code_inline"}]},{"text":" branch has been deleted post-merge (a common cleanup workflow).","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Key added ","type":"text"},{"text":"before","type":"text","marks":[{"type":"strong"}]},{"text":" that commit → Crowdin saw it on a prior export; translator made a decision; the absence on disk is a deliberate \"use English\" or \"leave blank\" choice. ","type":"text"},{"text":"Skip.","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Key added ","type":"text"},{"text":"after","type":"text","marks":[{"type":"strong"}]},{"text":" → Crowdin has not exported it yet; genuinely new and actionable.","type":"text"}]}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Latest Crowdin export reachable from HEAD (survives branch deletion).\nsync_ts=$(git log -1 --format=%ct --grep='^New Crowdin translations by GitHub Action

Find Missing Translations Overview Extract string resource keys from the default that are absent in a target locale's , excluding non-translatable entries. Outputs missing keys and offers to translate them. When to Use - Need to find untranslated strings for a specific locale - Preparing a batch of strings for a translator - Checking translation coverage after adding new features Background: Crowdin strip-identical behavior This repo syncs translations via Crowdin (branch ). Crowdin's default export behavior omits any translation that exactly equals the source , so a key that the translator d…

2>/dev/null)\nif [ -z \"$sync_ts\" ]; then\n echo \"WARNING: no Crowdin export commit found in history; treating all missing as actionable\" >&2\n sync_ts=0\nelse\n echo \"Crowdin sync cutoff: $(git log -1 --format='%ci %h' --grep='^New Crowdin translations by GitHub Action

Find Missing Translations Overview Extract string resource keys from the default that are absent in a target locale's , excluding non-translatable entries. Outputs missing keys and offers to translate them. When to Use - Need to find untranslated strings for a specific locale - Preparing a batch of strings for a translator - Checking translation coverage after adding new features Background: Crowdin strip-identical behavior This repo syncs translations via Crowdin (branch ). Crowdin's default export behavior omits any translation that exactly equals the source , so a key that the translator d…

)\"\nfi\n\n# For each locale, list only keys added after the Crowdin sync (truly new).\n# Run for BOTH \u003cstring> and \u003cplurals>.\nfor locale in cs-rCZ de-rDE sv-rSE; do\n echo \"=== $locale: genuinely new (post-sync) \u003cstring> keys ===\"\n comm -23 \\\n \u003c(grep '\u003cstring name=' amethyst/src/main/res/values/strings.xml \\\n | grep -v 'translatable=\"false\"' \\\n | sed 's/.*name=\"\\([^\"]*\\)\".*/\\1/' | sort) \\\n \u003c(grep '\u003cstring name=' amethyst/src/main/res/values-$locale/strings.xml \\\n | sed 's/.*name=\"\\([^\"]*\\)\".*/\\1/' | sort) \\\n | while IFS= read -r key; do\n added_ts=$(git log -1 --format=%ct -S \"name=\\\"$key\\\"\" -- amethyst/src/main/res/values/strings.xml)\n if [ -n \"$added_ts\" ] && [ \"$added_ts\" -gt \"$sync_ts\" ]; then\n echo \"$key\"\n fi\n done\n\n echo \"=== $locale: genuinely new (post-sync) \u003cplurals> keys ===\"\n comm -23 \\\n \u003c(grep '\u003cplurals name=' amethyst/src/main/res/values/strings.xml \\\n | sed 's/.*name=\"\\([^\"]*\\)\".*/\\1/' | sort) \\\n \u003c(grep '\u003cplurals name=' amethyst/src/main/res/values-$locale/strings.xml \\\n | sed 's/.*name=\"\\([^\"]*\\)\".*/\\1/' | sort) \\\n | while IFS= read -r key; do\n added_ts=$(git log -1 --format=%ct -S \"name=\\\"$key\\\"\" -- amethyst/src/main/res/values/strings.xml)\n if [ -n \"$added_ts\" ] && [ \"$added_ts\" -gt \"$sync_ts\" ]; then\n echo \"$key\"\n fi\n done\ndone","type":"text"}]},{"type":"paragraph","content":[{"text":"Why this beats using the ","type":"text","marks":[{"type":"strong"}]},{"text":"l10n_crowdin_translations","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" branch tip:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"The branch is often deleted after merge — the branch tip then doesn't exist or points to a stale ref.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"The branch tip may include Crowdin commits that haven't been merged to main yet. Those changes aren't in our working tree, so they don't affect what's on disk for us. The \"reachable from HEAD\" cutoff matches what we can actually observe in ","type":"text"},{"text":"values-*/strings.xml","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Only the listed (post-sync) keys are actionable.","type":"text","marks":[{"type":"strong"}]},{"text":" Anything older is either:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"A deliberate \"use English\" choice in Crowdin (brand terms like ","type":"text"},{"text":"Nowhere X","type":"text","marks":[{"type":"code_inline"}]},{"text":", loanwords like ","type":"text"},{"text":"Apps","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"Feed","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"Issues","type":"text","marks":[{"type":"code_inline"}]},{"text":", version prefixes like ","type":"text"},{"text":"v%1$s","type":"text","marks":[{"type":"code_inline"}]},{"text":"), or","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"A pending translation the translator hasn't filled in yet — still Crowdin's job, not ours.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"In both cases, Android's resource resolution falls back to ","type":"text"},{"text":"values/strings.xml","type":"text","marks":[{"type":"code_inline"}]},{"text":" at runtime, so there is no user-visible bug. Adding source-identical fallbacks locally is noise that the next sync will strip again.","type":"text"}]},{"type":"paragraph","content":[{"text":"Report the pre-sync skipped count as a one-liner (\"N keys predate the last Crowdin sync, skipped — Crowdin owns them\"). Do not list them or propose translations.","type":"text"}]},{"type":"paragraph","content":[{"text":"If no Crowdin export commit can be found in history (","type":"text"},{"text":"sync_ts=0","type":"text","marks":[{"type":"code_inline"}]},{"text":" fallback), warn the user and fall back to treating all missing keys as actionable — but flag that the workflow is degraded.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"3. Get English values for missing keys","type":"text"}]},{"type":"paragraph","content":[{"text":"For each missing key, extract its English value. ","type":"text"},{"text":"\u003cstring>","type":"text","marks":[{"type":"code_inline"}]},{"text":" is a single line; ","type":"text"},{"text":"\u003cplurals>","type":"text","marks":[{"type":"code_inline"}]},{"text":" is a multi-line block — handle each appropriately.","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Missing \u003cstring>: full line from default strings.xml\nwhile IFS= read -r key; do\n grep \"name=\\\"$key\\\"\" amethyst/src/main/res/values/strings.xml\ndone \u003c \u003c(comm -23 \\\n \u003c(grep '\u003cstring name=' amethyst/src/main/res/values/strings.xml \\\n | grep -v 'translatable=\"false\"' \\\n | sed 's/.*name=\"\\([^\"]*\\)\".*/\\1/' | sort) \\\n \u003c(grep '\u003cstring name=' amethyst/src/main/res/values-cs-rCZ/strings.xml \\\n | sed 's/.*name=\"\\([^\"]*\\)\".*/\\1/' | sort))\n\n# Missing \u003cplurals>: extract the multi-line block (opening tag through \u003c/plurals>)\nwhile IFS= read -r key; do\n awk -v key=\"$key\" '\n $0 ~ \"\u003cplurals name=\\\"\" key \"\\\"\" { in_p = 1 }\n in_p { print }\n in_p && /\u003c\\/plurals>/ { in_p = 0 }\n ' amethyst/src/main/res/values/strings.xml\ndone \u003c \u003c(comm -23 \\\n \u003c(grep '\u003cplurals name=' amethyst/src/main/res/values/strings.xml \\\n | sed 's/.*name=\"\\([^\"]*\\)\".*/\\1/' | sort) \\\n \u003c(grep '\u003cplurals name=' amethyst/src/main/res/values-cs-rCZ/strings.xml \\\n | sed 's/.*name=\"\\([^\"]*\\)\".*/\\1/' | sort))","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"4. Audit missing strings for plural-shaped patterns","type":"text"}]},{"type":"paragraph","content":[{"text":"Before presenting results, ","type":"text"},{"text":"scan the missing English strings","type":"text","marks":[{"type":"strong"}]},{"text":" for two red-flag patterns and warn the user about each match:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Hardcoded ","type":"text","marks":[{"type":"strong"}]},{"text":"\"1\"","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" next to a noun.","type":"text","marks":[{"type":"strong"}]},{"text":" A new English string like ","type":"text"},{"text":"\"1 reply\"","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"\"1 follower\"","type":"text","marks":[{"type":"code_inline"}]},{"text":", or ","type":"text"},{"text":"\"1 minute ago\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" almost always belongs in a ","type":"text"},{"text":"\u003cplurals>","type":"text","marks":[{"type":"code_inline"}]},{"text":" resource — not a ","type":"text"},{"text":"\u003cstring>","type":"text","marks":[{"type":"code_inline"}]},{"text":". Hardcoding ","type":"text"},{"text":"1","type":"text","marks":[{"type":"code_inline"}]},{"text":" in English forces every translator to either also hardcode ","type":"text"},{"text":"1","type":"text","marks":[{"type":"code_inline"}]},{"text":" (breaking languages where the ","type":"text"},{"text":"one","type":"text","marks":[{"type":"code_inline"}]},{"text":" category covers other numbers, e.g. some Slavic languages) or to silently change the meaning.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"A ","type":"text","marks":[{"type":"strong"}]},{"text":"%d","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" / ","type":"text","marks":[{"type":"strong"}]},{"text":"%1$d","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" placeholder in a clearly singular/plural sentence","type":"text","marks":[{"type":"strong"}]},{"text":" (e.g. ","type":"text"},{"text":"\"%1$d reply\"","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"\"%d follower\"","type":"text","marks":[{"type":"code_inline"}]},{"text":"). Even though the placeholder is parameterised, English-only ","type":"text"},{"text":"one","type":"text","marks":[{"type":"code_inline"}]},{"text":"/","type":"text"},{"text":"other","type":"text","marks":[{"type":"code_inline"}]},{"text":" agreement won't survive translation into languages that need ","type":"text"},{"text":"few","type":"text","marks":[{"type":"code_inline"}]},{"text":"/","type":"text"},{"text":"many","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Also ","type":"text"},{"text":"audit existing ","type":"text","marks":[{"type":"strong"}]},{"text":"\u003cplurals>","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" resources","type":"text","marks":[{"type":"strong"}]},{"text":" for two anti-patterns:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"quantity=\"one\"","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" items that hardcode the literal ","type":"text","marks":[{"type":"strong"}]},{"text":"1","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" (instead of using a ","type":"text"},{"text":"%d","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"%1$d","type":"text","marks":[{"type":"code_inline"}]},{"text":" placeholder) — broken for languages where the ","type":"text"},{"text":"one","type":"text","marks":[{"type":"code_inline"}]},{"text":" CLDR category covers more than just ","type":"text"},{"text":"n=1","type":"text","marks":[{"type":"code_inline"}]},{"text":" (Russian, Ukrainian, Croatian, etc.).","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"quantity=\"zero\"","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" items in any locale that doesn't natively use the ","type":"text","marks":[{"type":"strong"}]},{"text":"zero","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" CLDR category","type":"text","marks":[{"type":"strong"}]},{"text":" — i.e. ","type":"text"},{"text":"everything except Arabic (","type":"text","marks":[{"type":"strong"}]},{"text":"ar","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":") and Welsh (","type":"text","marks":[{"type":"strong"}]},{"text":"cy","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":")","type":"text","marks":[{"type":"strong"}]},{"text":". ICU/CLDR maps ","type":"text"},{"text":"count=0","type":"text","marks":[{"type":"code_inline"}]},{"text":" to ","type":"text"},{"text":"other","type":"text","marks":[{"type":"code_inline"}]},{"text":" for English and all the locales we ship to (cs, de, pt-BR, sv, etc.), so ","type":"text"},{"text":"\u003citem quantity=\"zero\">","type":"text","marks":[{"type":"code_inline"}]},{"text":" is ","type":"text"},{"text":"dead code","type":"text","marks":[{"type":"strong"}]},{"text":" there: ","type":"text"},{"text":"getQuantityString(id, 0)","type":"text","marks":[{"type":"code_inline"}]},{"text":" will pick ","type":"text"},{"text":"other","type":"text","marks":[{"type":"code_inline"}]},{"text":", never the zero entry, and the visible runtime string ends up ","type":"text"},{"text":"\"…0 items\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" instead of the intended ","type":"text"},{"text":"\"…no items\"","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"If a UX genuinely wants special \"no items\" wording at count=0, that has to be a call-site ","type":"text"},{"text":"if (count == 0)","type":"text","marks":[{"type":"code_inline"}]},{"text":" branch to a separate ","type":"text"},{"text":"\u003cstring>","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"not","type":"text","marks":[{"type":"strong"}]},{"text":" a ","type":"text"},{"text":"quantity=\"zero\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" plural item.","type":"text"}]},{"type":"paragraph","content":[{"text":"Flag and offer to fix:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Scan every locale's strings.xml for \u003citem quantity=\"one\"> entries that\n# hardcode \"1\" (or other literal digits) instead of using a placeholder.\n# Looks at default + all values-* locales.\nfor f in amethyst/src/main/res/values/strings.xml amethyst/src/main/res/values-*/strings.xml; do\n awk -v file=\"$f\" '\n /\u003cplurals/ { in_plurals = 1; name = $0; sub(/.*name=\"/, \"\", name); sub(/\".*/, \"\", name) }\n in_plurals && /quantity=\"one\"/ {\n # Extract item text (between > and \u003c)\n text = $0; sub(/^[^>]*>/, \"\", text); sub(/\u003c.*$/, \"\", text)\n # Flag if it contains a digit AND no %d / %1$d placeholder\n if (text ~ /[0-9]/ && text !~ /%[0-9]*\\$?d/) {\n print file \": \u003cplurals name=\\\"\" name \"\\\"> one=\\\"\" text \"\\\"\"\n }\n }\n /\u003c\\/plurals>/ { in_plurals = 0 }\n ' \"$f\"\ndone","type":"text"}]},{"type":"paragraph","content":[{"text":"Then scan for dead ","type":"text"},{"text":"quantity=\"zero\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" entries. CLDR's ","type":"text"},{"text":"zero","type":"text","marks":[{"type":"code_inline"}]},{"text":" category is integer-bearing only in ","type":"text"},{"text":"Arabic (","type":"text","marks":[{"type":"strong"}]},{"text":"ar","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":")","type":"text","marks":[{"type":"strong"}]},{"text":" and ","type":"text"},{"text":"Welsh (","type":"text","marks":[{"type":"strong"}]},{"text":"cy","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":")","type":"text","marks":[{"type":"strong"}]},{"text":". In every other locale, count=0 falls through to ","type":"text"},{"text":"other","type":"text","marks":[{"type":"code_inline"}]},{"text":", so a ","type":"text"},{"text":"\u003citem quantity=\"zero\">","type":"text","marks":[{"type":"code_inline"}]},{"text":" entry is dead and likely a translator/author bug (or it silently never fires):","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"for f in amethyst/src/main/res/values/strings.xml amethyst/src/main/res/values-*/strings.xml; do\n # Skip Arabic and Welsh — they natively use the zero category.\n case \"$f\" in\n *values-ar*|*values-cy*) continue ;;\n esac\n awk -v file=\"$f\" '\n /\u003cplurals/ { in_plurals = 1; name = $0; sub(/.*name=\"/, \"\", name); sub(/\".*/, \"\", name) }\n in_plurals && /quantity=\"zero\"/ {\n text = $0; sub(/^[^>]*>/, \"\", text); sub(/\u003c.*$/, \"\", text)\n print file \": \u003cplurals name=\\\"\" name \"\\\"> zero=\\\"\" text \"\\\"\"\n }\n /\u003c\\/plurals>/ { in_plurals = 0 }\n ' \"$f\"\ndone","type":"text"}]},{"type":"paragraph","content":[{"text":"For each hit, warn the user that the entry is unreachable in that locale. The fix is to ","type":"text"},{"text":"remove the ","type":"text","marks":[{"type":"strong"}]},{"text":"\u003citem quantity=\"zero\">","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" and, if the UX wanted distinct wording for count=0, add a separate ","type":"text"},{"text":"\u003cstring>","type":"text","marks":[{"type":"code_inline"}]},{"text":" plus an ","type":"text"},{"text":"if (count == 0)","type":"text","marks":[{"type":"code_inline"}]},{"text":" branch at the call site (see \"Plurals: handle with care\" below).","type":"text"}]},{"type":"paragraph","content":[{"text":"Quick scan over the missing keys:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Flag missing English values that look like they should be \u003cplurals>\nwhile IFS= read -r key; do\n line=$(grep \"name=\\\"$key\\\"\" amethyst/src/main/res/values/strings.xml)\n # Hardcoded standalone \"1\" (word-boundary), or a count placeholder followed by a likely-countable noun\n if echo \"$line\" | grep -qE '>([^\u003c]*\\b1\\b[^\u003c]*|[^\u003c]*%[0-9]*\\$?d[^\u003c]*)\u003c'; then\n echo \"PLURAL CANDIDATE: $line\"\n fi\ndone \u003c \u003c(comm -23 \\\n \u003c(grep '\u003cstring name=' amethyst/src/main/res/values/strings.xml \\\n | grep -v 'translatable=\"false\"' \\\n | sed 's/.*name=\"\\([^\"]*\\)\".*/\\1/' | sort) \\\n \u003c(grep '\u003cstring name=' amethyst/src/main/res/values-cs-rCZ/strings.xml \\\n | sed 's/.*name=\"\\([^\"]*\\)\".*/\\1/' | sort))","type":"text"}]},{"type":"paragraph","content":[{"text":"The regex is intentionally noisy — review each hit by hand. Many ","type":"text"},{"text":"%d","type":"text","marks":[{"type":"code_inline"}]},{"text":" strings (e.g. ","type":"text"},{"text":"\"Limits for kind %1$d\"","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"\"Max event size (bytes)\"","type":"text","marks":[{"type":"code_inline"}]},{"text":") are ","type":"text"},{"text":"not","type":"text","marks":[{"type":"em"}]},{"text":" plural-bearing. Only flag the ones whose surrounding noun changes form with the count.","type":"text"}]},{"type":"paragraph","content":[{"text":"For each genuine match, ","type":"text"},{"text":"stop and warn the user before translating","type":"text","marks":[{"type":"strong"}]},{"text":", e.g.:","type":"text"}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"⚠️ ","type":"text"},{"text":"notification_count","type":"text","marks":[{"type":"code_inline"}]},{"text":" is ","type":"text"},{"text":"\"1 new reply\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" — this hardcodes ","type":"text"},{"text":"\"1\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" and should likely be a ","type":"text"},{"text":"\u003cplurals>","type":"text","marks":[{"type":"code_inline"}]},{"text":" resource (e.g. ","type":"text"},{"text":"quantity=\"one\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" → ","type":"text"},{"text":"\"%d new reply\"","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"quantity=\"other\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" → ","type":"text"},{"text":"\"%d new replies\"","type":"text","marks":[{"type":"code_inline"}]},{"text":"). Convert before translating?","type":"text"}]}]},{"type":"paragraph","content":[{"text":"Do not silently translate plural-shaped ","type":"text"},{"text":"\u003cstring>","type":"text","marks":[{"type":"code_inline"}]},{"text":" entries; the wrong shape will then need to be fixed in every locale.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"5. Present results and ask to translate","type":"text"}]},{"type":"paragraph","content":[{"text":"Output the missing entries as raw XML resource lines (copy-paste ready):","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"xml"},"content":[{"text":" \u003cstring name=\"attestation_valid\">Valid\u003c/string>\n \u003cstring name=\"attestation_valid_from\">Valid from %1$s\u003c/string>\n \u003cstring name=\"feed_group_lists\">Lists\u003c/string>","type":"text"}]},{"type":"paragraph","content":[{"text":"Also check ","type":"text"},{"text":"\u003cstring-array>","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"\u003cplurals>","type":"text","marks":[{"type":"code_inline"}]},{"text":" tags using the same approach if the project uses them.","type":"text"}]},{"type":"heading","attrs":{"level":4},"content":[{"text":"Plurals: handle with care","type":"text"}]},{"type":"paragraph","content":[{"text":"When adding or proposing ","type":"text"},{"text":"\u003cplurals>","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" entries, follow these rules:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Never hardcode ","type":"text","marks":[{"type":"strong"}]},{"text":"\"1\"","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" in the English text of a ","type":"text"},{"text":"quantity=\"one\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" item. Use the format placeholder (e.g. ","type":"text"},{"text":"%1$d","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"%d","type":"text","marks":[{"type":"code_inline"}]},{"text":") so the runtime substitutes the actual count. Hardcoding ","type":"text"},{"text":"\"1\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" breaks every language whose ","type":"text"},{"text":"one","type":"text","marks":[{"type":"code_inline"}]},{"text":" category covers numbers other than 1 (e.g. some Slavic languages).","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Don't assume ","type":"text","marks":[{"type":"strong"}]},{"text":"one","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" + ","type":"text","marks":[{"type":"strong"}]},{"text":"other","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" is enough.","type":"text","marks":[{"type":"strong"}]},{"text":" CLDR plural categories vary by language: ","type":"text"},{"text":"zero","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"one","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"two","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"few","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"many","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"other","type":"text","marks":[{"type":"code_inline"}]},{"text":". Always include ","type":"text"},{"text":"every category the target language uses","type":"text","marks":[{"type":"strong"}]},{"text":", not just the categories present in English. Examples:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"English (","type":"text"},{"text":"en","type":"text","marks":[{"type":"code_inline"}]},{"text":"): ","type":"text"},{"text":"one","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"other","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Czech (","type":"text"},{"text":"cs","type":"text","marks":[{"type":"code_inline"}]},{"text":"): ","type":"text"},{"text":"one","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"few","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"many","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"other","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Polish (","type":"text"},{"text":"pl","type":"text","marks":[{"type":"code_inline"}]},{"text":"): ","type":"text"},{"text":"one","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"few","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"many","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"other","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Russian (","type":"text"},{"text":"ru","type":"text","marks":[{"type":"code_inline"}]},{"text":"): ","type":"text"},{"text":"one","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"few","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"many","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"other","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Arabic (","type":"text"},{"text":"ar","type":"text","marks":[{"type":"code_inline"}]},{"text":"): ","type":"text"},{"text":"zero","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"one","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"two","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"few","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"many","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"other","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"German / Swedish / Brazilian Portuguese: ","type":"text"},{"text":"one","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"other","type":"text","marks":[{"type":"code_inline"}]}]}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"When a missing string contains a count placeholder and is conceptually a singular/plural pair, ","type":"text"},{"text":"flag it before translating","type":"text","marks":[{"type":"strong"}]},{"text":" — it may belong as a ","type":"text"},{"text":"\u003cplurals>","type":"text","marks":[{"type":"code_inline"}]},{"text":" resource rather than a single ","type":"text"},{"text":"\u003cstring>","type":"text","marks":[{"type":"code_inline"}]},{"text":". Surface this to the user before proposing translations.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Do not use ","type":"text","marks":[{"type":"strong"}]},{"text":"quantity=\"zero\"","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" outside Arabic (","type":"text","marks":[{"type":"strong"}]},{"text":"ar","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":") and Welsh (","type":"text","marks":[{"type":"strong"}]},{"text":"cy","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":").","type":"text","marks":[{"type":"strong"}]},{"text":" CLDR's ","type":"text"},{"text":"zero","type":"text","marks":[{"type":"code_inline"}]},{"text":" category is integer-bearing only in those two languages. Android calls ","type":"text"},{"text":"PluralRules.select(0)","type":"text","marks":[{"type":"code_inline"}]},{"text":" for the device locale; in English/German/Czech/Polish/Russian/Swedish/Portuguese/etc. it returns ","type":"text"},{"text":"other","type":"text","marks":[{"type":"code_inline"}]},{"text":", so the explicit ","type":"text"},{"text":"\u003citem quantity=\"zero\">","type":"text","marks":[{"type":"code_inline"}]},{"text":" is never picked at runtime and the user sees ","type":"text"},{"text":"\"…0 items\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" instead of the intended wording. If the design calls for \"no items\" at count=0, model it as a separate ","type":"text"},{"text":"\u003cstring>","type":"text","marks":[{"type":"code_inline"}]},{"text":" and an ","type":"text"},{"text":"if (count == 0)","type":"text","marks":[{"type":"code_inline"}]},{"text":" branch at the call site:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"kotlin"},"content":[{"text":"val label = if (count == 0) {\n stringRes(R.string.foo_no_items, dateLabel)\n} else {\n pluralStringResource(R.plurals.foo_items, count, dateLabel, count)\n}","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Reference: ","type":"text"},{"text":"Android ","type":"text","marks":[{"type":"link","attrs":{"href":"https://developer.android.com/guide/topics/resources/string-resource#Plurals","title":null}}]},{"text":"\u003cplurals>","type":"text","marks":[{"type":"link","attrs":{"href":"https://developer.android.com/guide/topics/resources/string-resource#Plurals","title":null}},{"type":"code_inline"}]},{"text":" docs","type":"text","marks":[{"type":"link","attrs":{"href":"https://developer.android.com/guide/topics/resources/string-resource#Plurals","title":null}}]},{"text":" and ","type":"text"},{"text":"CLDR plural rules","type":"text","marks":[{"type":"link","attrs":{"href":"https://unicode-org.github.io/cldr-staging/charts/latest/supplemental/language_plural_rules.html","title":null}}]},{"text":".","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Then ask the user:","type":"text","marks":[{"type":"strong"}]},{"text":" \"Would you like me to translate these missing strings into [list of target locales]?\"","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"6. Adding translations (if approved)","type":"text"}]},{"type":"paragraph","content":[{"text":"When adding translated strings to locale files:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Append new strings at the bottom","type":"text","marks":[{"type":"strong"}]},{"text":" of the file, just before the closing ","type":"text"},{"text":"\u003c/resources>","type":"text","marks":[{"type":"code_inline"}]},{"text":" tag.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Do NOT try to insert them in alphabetical or matching order — a separate process handles ordering.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Common Mistakes","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Forgetting ","type":"text","marks":[{"type":"strong"}]},{"text":"translatable=\"false\"","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" — these should never appear in locale files","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Diffing only ","type":"text","marks":[{"type":"strong"}]},{"text":"\u003cstring name=","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" — ","type":"text"},{"text":"\u003cplurals>","type":"text","marks":[{"type":"code_inline"}]},{"text":" is a separate resource type; a source ","type":"text"},{"text":"\u003cplurals>","type":"text","marks":[{"type":"code_inline"}]},{"text":" missing from a locale will never show up in a ","type":"text"},{"text":"\u003cstring>","type":"text","marks":[{"type":"code_inline"}]},{"text":" diff. Always run the diff twice (once per resource type) as shown in Step 2. The same goes for ","type":"text"},{"text":"\u003cstring-array>","type":"text","marks":[{"type":"code_inline"}]},{"text":" if the project uses it.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Treating every missing key as actionable","type":"text","marks":[{"type":"strong"}]},{"text":" — Crowdin strips on export any translation the translator marked as \"use English\", and we cannot distinguish that from \"never seen\" by looking at disk. Use the Step 2.5 sync-timestamp filter: only keys added to ","type":"text"},{"text":"values/strings.xml","type":"text","marks":[{"type":"code_inline"}]},{"text":" after the last ","type":"text"},{"text":"l10n_crowdin_translations","type":"text","marks":[{"type":"code_inline"}]},{"text":" sync are genuinely new.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Trying to detect \"stripped\" from git history alone","type":"text","marks":[{"type":"strong"}]},{"text":" — the on-disk locale file only sees keys the translator typed a non-identical value for. The \"translator opened the key and picked English from the start\" case never touches disk, so a history-only check misses it. Use the sync-timestamp cutoff instead.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Adding source-identical fallbacks locally","type":"text","marks":[{"type":"strong"}]},{"text":" — they get overwritten on the next Crowdin sync. Android falls back to ","type":"text"},{"text":"values/strings.xml","type":"text","marks":[{"type":"code_inline"}]},{"text":" at runtime anyway, so there is no user-visible bug to fix.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Skipping per-locale diffs when only diffing cs-rCZ","type":"text","marks":[{"type":"strong"}]},{"text":" — Crowdin can strip different keys in different locales (each translator's choice), so cs-rCZ is not a reliable upper bound. Diff each target locale, then apply the sync-timestamp filter.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Inserting strings in a specific position","type":"text","marks":[{"type":"strong"}]},{"text":" — always append at the bottom; ordering is handled separately","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Hardcoding ","type":"text","marks":[{"type":"strong"}]},{"text":"\"1\"","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" in a ","type":"text","marks":[{"type":"strong"}]},{"text":"\u003cplurals>","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" ","type":"text","marks":[{"type":"strong"}]},{"text":"quantity=\"one\"","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" item","type":"text","marks":[{"type":"strong"}]},{"text":" — always use the count placeholder; otherwise non-English ","type":"text"},{"text":"one","type":"text","marks":[{"type":"code_inline"}]},{"text":" categories produce wrong text","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Copying English's ","type":"text","marks":[{"type":"strong"}]},{"text":"one","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":"/","type":"text","marks":[{"type":"strong"}]},{"text":"other","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" set into every locale","type":"text","marks":[{"type":"strong"}]},{"text":" — each language must include all CLDR plural categories it uses (e.g. Czech needs ","type":"text"},{"text":"one","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"few","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"many","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"other","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Using ","type":"text","marks":[{"type":"strong"}]},{"text":"\u003citem quantity=\"zero\">","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" to special-case count=0","type":"text","marks":[{"type":"strong"}]},{"text":" — outside Arabic and Welsh, this entry is unreachable: ICU/CLDR maps 0 → ","type":"text"},{"text":"other","type":"text","marks":[{"type":"code_inline"}]},{"text":", so the runtime never picks the zero item and the user sees ","type":"text"},{"text":"\"…0 items\"","type":"text","marks":[{"type":"code_inline"}]},{"text":". Special-case at the call site with a separate ","type":"text"},{"text":"\u003cstring>","type":"text","marks":[{"type":"code_inline"}]},{"text":" instead.","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}}]},"metadata":{"date":"2026-06-05","name":"find-missing-translations","author":"@skillopedia","source":{"stars":1538,"repo_name":"amethyst","origin_url":"https://github.com/vitorpamplona/amethyst/blob/HEAD/.claude/skills/find-missing-translations/SKILL.md","repo_owner":"vitorpamplona","body_sha256":"6a7ea6ac8f51bef0f8d3166903a6683f99b8eb7aab245a58a299e7c1267dd3bc","cluster_key":"3a63a24ee7c8103d3a54e0a46b1bcbc9ec1c2eadb88d103f46a6276f5089df60","clean_bundle":{"format":"clean-skill-bundle-v1","source":"vitorpamplona/amethyst/.claude/skills/find-missing-translations/SKILL.md","bundle_sha256":"801b72bcf60ec0cfe98cde092ec34f492f196883e5ffd12b56e1585622a026f9","attachment_count":0,"text_attachments":0,"binary_attachments":0},"cluster_size":1,"skill_md_path":".claude/skills/find-missing-translations/SKILL.md","import_metadata":{"date":"2026-06-05","author":"@skillopedia","version":"v1","category":"general","category_label":"General"},"exact_dupes_collapsed_into_this":0},"version":"v1","category":"general","import_tag":"clean-skills-v1","description":"Use when comparing Android strings.xml locale files to find untranslated string resources, missing translation keys, or preparing translation work for a specific language"}},"renderedAt":1782981534824}

Find Missing Translations Overview Extract string resource keys from the default that are absent in a target locale's , excluding non-translatable entries. Outputs missing keys and offers to translate them. When to Use - Need to find untranslated strings for a specific locale - Preparing a batch of strings for a translator - Checking translation coverage after adding new features Background: Crowdin strip-identical behavior This repo syncs translations via Crowdin (branch ). Crowdin's default export behavior omits any translation that exactly equals the source , so a key that the translator d…