|
| 1 | +# frozen_string_literal: true |
| 2 | + |
| 3 | +require_relative 'plural_strings_helper' |
| 4 | + |
| 5 | +################################################# |
| 6 | +# Plurals: String Catalog ⇄ GlotPress ("all-flat") |
| 7 | +# |
| 8 | +# Plurals are authored in `WordPress/Classes/Plurals.xcstrings` (English one/other) and carried through the |
| 9 | +# MAIN app GlotPress project as flat strings keyed `<key>|==|plural.<cldr-category>` — the same id Apple's |
| 10 | +# `xcodebuild -exportLocalizations` uses — so every locale (incl. Welsh) is covered. The forward merges these |
| 11 | +# flat originals into `Localizable.strings` (like MANUALLY_MAINTAINED_STRINGS_FILES); the reverse reads them |
| 12 | +# back out of the downloaded `Localizable.strings` and folds them into the catalog JSON (build-free) via the |
| 13 | +# committed per-locale category map. The flat keys stay in `Localizable.strings` as harmless, unused-at-runtime |
| 14 | +# entries — exactly like the merged `infoplist.*` keys. The exporter is consulted only by |
| 15 | +# `refresh_plural_categories` when the ship-locale list changes. |
| 16 | +################################################# |
| 17 | + |
| 18 | +# Lives in a synchronized source folder (WordPress/Classes) so it auto-joins the WordPress target. |
| 19 | +# `WordPress/Resources` is an explicitly-referenced (non-synchronized) group, so a catalog placed |
| 20 | +# there is NOT a target member and would be skipped by `-exportLocalizations`. |
| 21 | +PLURALS_CATALOG = File.join(PROJECT_ROOT_FOLDER, 'WordPress', 'Classes', 'Plurals.xcstrings') |
| 22 | +PLURALS_FLAT_STRINGS = File.join(PROJECT_ROOT_FOLDER, 'WordPress', 'Resources', 'Plurals.strings') # transient merge input (not committed) |
| 23 | +# Per-locale CLDR category map ({ "<lproj>" => ["one","other",…] }): drives the build-free reverse (which slots |
| 24 | +# each locale needs) and the forward union (which originals to upload). Captured from Apple's exporter (CLDR) |
| 25 | +# over the 33 ship locales; regenerate with `refresh_plural_categories` when the supported-locale list changes. |
| 26 | +PLURAL_CATEGORIES_JSON = File.join(PROJECT_ROOT_FOLDER, 'WordPress', 'Resources', 'plural-categories.json') |
| 27 | +PLURALS_SCHEME = 'WordPress' |
| 28 | + |
| 29 | +platform :ios do |
| 30 | + # FORWARD (no build): Plurals.xcstrings (English) -> flat "<key>|==|plural.<cat>" originals (a transient |
| 31 | + # `.strings` that `generate_strings_file_for_glotpress` merges into Localizable.strings for the main project). |
| 32 | + # |
| 33 | + # Called by generate_strings_file_for_glotpress (its originals merge into Localizable.strings). |
| 34 | + desc 'Generates the flat plural originals (.strings) merged into Localizable.strings for GlotPress' |
| 35 | + lane :generate_plural_strings_for_glotpress do |
| 36 | + catalog = JSON.parse(File.read(PLURALS_CATALOG)) |
| 37 | + categories = PluralStrings.union_categories(JSON.parse(File.read(PLURAL_CATEGORIES_JSON))) |
| 38 | + |
| 39 | + missing = PluralStrings.plural_keys_missing_other(catalog) |
| 40 | + unless missing.empty? |
| 41 | + UI.user_error!("Plurals.xcstrings: plural(s) missing a non-empty English `other` form (CLDR requires it — without it they upload empty originals): #{missing.join(', ')}") |
| 42 | + end |
| 43 | + |
| 44 | + originals = PluralStrings.flat_originals(catalog, categories) |
| 45 | + File.write(PLURALS_FLAT_STRINGS, PluralStrings.serialize_legacy_strings(originals)) |
| 46 | + UI.message("Generated #{originals.size} flat plural originals from #{catalog['strings'].size} catalog keys → #{PLURALS_FLAT_STRINGS}") |
| 47 | + end |
| 48 | + |
| 49 | + # REVERSE (no build): pull the flat plural translations back out of the already-downloaded app |
| 50 | + # `Localizable.strings` (they rode the main GlotPress project) and fold them straight into Plurals.xcstrings |
| 51 | + # JSON, using the committed per-locale category map. Each cell is human ?? AI ?? English source; machine and |
| 52 | + # English-fallback cells are flagged needs_review. |
| 53 | + # |
| 54 | + # Called by download_localized_strings, after the app strings are downloaded. |
| 55 | + desc 'Folds plural translations from the downloaded Localizable.strings into Plurals.xcstrings' |
| 56 | + lane :download_localized_plurals do |
| 57 | + catalog = JSON.parse(File.read(PLURALS_CATALOG)) |
| 58 | + missing = PluralStrings.plural_keys_missing_other(catalog) |
| 59 | + UI.user_error!("Plurals.xcstrings: plural(s) missing a non-empty English `other` form (CLDR requires it): #{missing.join(', ')}") unless missing.empty? |
| 60 | + categories_by_locale = JSON.parse(File.read(PLURAL_CATEGORIES_JSON)) |
| 61 | + |
| 62 | + written = PluralStrings.fold_translations!( |
| 63 | + catalog, |
| 64 | + categories_by_locale: categories_by_locale, |
| 65 | + translations_by_locale: plural_translations_by_locale(File.join(PROJECT_ROOT_FOLDER, 'WordPress', 'Resources')), |
| 66 | + ai_translator: method(:ai_translate_plural) |
| 67 | + ) |
| 68 | + File.write(PLURALS_CATALOG, "#{JSON.pretty_generate(catalog)}\n") |
| 69 | + UI.message("Folded plural translations from Localizable.strings into #{File.basename(PLURALS_CATALOG)} (#{written} locale variations).") |
| 70 | + |
| 71 | + git_commit(path: [PLURALS_CATALOG], message: 'Update plural translations from GlotPress', allow_nothing_to_commit: true) |
| 72 | + end |
| 73 | + |
| 74 | + # Regenerate the per-locale CLDR category map (`plural-categories.json`) from Apple's exporter over the ship |
| 75 | + # locales. Run only when the supported-locale list changes — the one place the exporter is used. Build-backed. |
| 76 | + desc 'Refreshes plural-categories.json (per-locale CLDR sets) from the exporter over the ship locales' |
| 77 | + lane :refresh_plural_categories do |
| 78 | + categories_by_locale = export_plural_skeletons(GLOTPRESS_TO_LPROJ_APP_LOCALE_CODES.values.uniq) do |skeleton_dir| |
| 79 | + paths = Dir.glob(File.join(skeleton_dir, '*.xcloc', 'Localized Contents', '*.xliff')) |
| 80 | + PluralStrings.categories_by_locale_from_skeletons(paths) |
| 81 | + end |
| 82 | + UI.user_error!('No plural categories found — is there a plural in Plurals.xcstrings?') if categories_by_locale.empty? |
| 83 | + |
| 84 | + File.write(PLURAL_CATEGORIES_JSON, "#{JSON.pretty_generate(categories_by_locale)}\n") |
| 85 | + UI.success("Wrote plural categories for #{categories_by_locale.size} locales: #{categories_by_locale.keys.sort.join(', ')}") |
| 86 | + end |
| 87 | + |
| 88 | + ################################################# |
| 89 | + # Helpers |
| 90 | + ################################################# |
| 91 | + |
| 92 | + # Exports the per-locale plural skeletons (one build) into a temp dir and yields it, removing the dir when |
| 93 | + # the block returns. Returns whatever the block returns. |
| 94 | + # |
| 95 | + # `SUPPORTS_MACCATALYST=NO` constrains the string-extraction build to iOS. Without it, |
| 96 | + # `-exportLocalizations` builds every supported destination incl. Mac Catalyst, which fails |
| 97 | + # when a binary dependency (e.g. a Zendesk xcframework) ships no `maccatalyst` slice. |
| 98 | + def export_plural_skeletons(xcode_locales) |
| 99 | + Dir.mktmpdir do |out| |
| 100 | + sh( |
| 101 | + 'xcodebuild', '-exportLocalizations', |
| 102 | + '-workspace', WORKSPACE_PATH, |
| 103 | + '-scheme', PLURALS_SCHEME, |
| 104 | + '-localizationPath', out, |
| 105 | + 'SUPPORTS_MACCATALYST=NO', |
| 106 | + *xcode_locales.flat_map { |loc| ['-exportLanguage', loc] } |
| 107 | + ) |
| 108 | + yield out |
| 109 | + end |
| 110 | + end |
| 111 | + |
| 112 | + # Pulls the flat plural keys out of each locale's downloaded `Localizable.strings`, returning |
| 113 | + # { "<lproj>" => { "<flat-plural-id>" => value } }. Decoding (escapes like `\n`/`\U…`, encoding/BOM) is |
| 114 | + # delegated to `L10nHelper.read_strings_file_as_hash` — Apple's `plutil` — rather than a hand-rolled parser. |
| 115 | + def plural_translations_by_locale(dir) |
| 116 | + Dir.glob(File.join(dir, '*.lproj', 'Localizable.strings')).each_with_object({}) do |path, acc| |
| 117 | + locale = File.basename(File.dirname(path), '.lproj') |
| 118 | + translations = Fastlane::Helper::Ios::L10nHelper.read_strings_file_as_hash(path: path) |
| 119 | + acc[locale] = translations.select { |key, _| PluralStrings.plural_key?(key) } |
| 120 | + end |
| 121 | + end |
| 122 | + |
| 123 | + # Machine-translation floor for the reverse fold: invoked for every plural slot with no human translation. |
| 124 | + # Returns nil until wired to a translation service, leaving such slots to fall back to the English source |
| 125 | + # (flagged needs_review). The named `category` + dev `note` let the prompt request the correct grammatical |
| 126 | + # form (e.g. "give me the Polish *few* form of …"). |
| 127 | + # rubocop:disable Lint/UnusedMethodArgument -- keyword names are the documented call contract |
| 128 | + def ai_translate_plural(id:, source:, category:, note:, locale:) |
| 129 | + nil # TODO: call the translation service. |
| 130 | + end |
| 131 | + # rubocop:enable Lint/UnusedMethodArgument |
| 132 | +end |
0 commit comments