|
4 | 4 | require 'tmpdir' |
5 | 5 | require 'fileutils' |
6 | 6 | require_relative 'catalog_helper' |
| 7 | +require_relative 'plural_strings_helper' |
7 | 8 |
|
8 | 9 | ################################################# |
9 | 10 | # Catalog generation (forward / extraction) |
|
39 | 40 | # The custom localization routine to additionally extract (same as the genstrings `routines:` today). |
40 | 41 | CATALOG_LOCALIZATION_ROUTINE = 'AppLocalizedString' |
41 | 42 |
|
| 43 | +# App Intents display strings (LocalizedStringResource) can't be parsed by genstrings, so the code |
| 44 | +# freeze extracts them from these dirs with xcstringstool and merges them into the GlotPress upload |
| 45 | +# (see generate_app_intents_strings_for_glotpress). Keys in code are self-qualified with these prefixes. |
| 46 | +APP_INTENTS_STRINGS_ROOTS = [ |
| 47 | + File.join(PROJECT_ROOT_FOLDER, 'Modules', 'Sources', 'JetpackStatsWidgetsCore', 'AppIntents') |
| 48 | +].freeze |
| 49 | + |
| 50 | +APP_INTENTS_KEY_PREFIXES = [ |
| 51 | + 'ios-widget.' |
| 52 | +].freeze |
| 53 | + |
42 | 54 | platform :ios do |
43 | 55 | # Extracts English source strings from code into Localizable.xcstrings (build-free; replaces genstrings). |
44 | 56 | # |
@@ -200,4 +212,59 @@ def report_catalog(path, extracted_count:, reconciled_count:) |
200 | 212 | message += " Re-flagged #{reconciled_count} for review (English source changed)." if reconciled_count.positive? |
201 | 213 | UI.success(message) |
202 | 214 | end |
| 215 | + |
| 216 | + # Extracts the App Intents LocalizedStringResource strings (key, English defaultValue, translator |
| 217 | + # comment) from source and returns them as `.strings` file content for the GlotPress merge. The keys |
| 218 | + # are already prefixed in code, so the caller merges this with an empty prefix, like the plural |
| 219 | + # originals. Fails loudly rather than silently shipping an untranslated intent string. |
| 220 | + # Declared as a lane like generate_plural_strings_for_glotpress: the freeze lane calls it as a |
| 221 | + # function, and it can be run standalone as a smoke check (prints the count; errors on unprefixed keys). |
| 222 | + desc 'Generates the App Intents strings content that the code freeze merges into the GlotPress upload' |
| 223 | + lane :generate_app_intents_strings_for_glotpress do |
| 224 | + files = catalog_source_files(APP_INTENTS_STRINGS_ROOTS) |
| 225 | + UI.user_error!('No App Intents source files found — APP_INTENTS_STRINGS_ROOTS out of date?') if files.empty? |
| 226 | + |
| 227 | + entries = Dir.mktmpdir do |tmp| |
| 228 | + extract_stringsdata(files: files, output_dir: tmp) |
| 229 | + app_intents_entries(stringsdata_dir: tmp) |
| 230 | + end |
| 231 | + UI.user_error!('No App Intents strings extracted') if entries.empty? |
| 232 | + |
| 233 | + unprefixed = entries.keys.reject { |key| APP_INTENTS_KEY_PREFIXES.any? { |prefix| key.start_with?(prefix) } } |
| 234 | + UI.user_error!("App Intents strings without a known prefix (add one, or extend APP_INTENTS_KEY_PREFIXES): #{unprefixed.sort}") unless unprefixed.empty? |
| 235 | + |
| 236 | + UI.message("Extracted #{entries.count} App Intents strings for the GlotPress upload.") |
| 237 | + PluralStrings.serialize_legacy_strings(entries.sort.to_h) # sorted for stable output |
| 238 | + end |
| 239 | + |
| 240 | + # { key => { value:, comment: } } from a scoped extraction, via a throwaway catalog (like |
| 241 | + # current_english_values). Entries without an explicit English value are interpolation-only |
| 242 | + # resources (e.g. DisplayRepresentation(title: "\(name)")) — not translatable text — and are skipped. |
| 243 | + def app_intents_entries(stringsdata_dir:) |
| 244 | + Dir.mktmpdir do |tmp| |
| 245 | + fresh = File.join(tmp, 'Localizable.xcstrings') |
| 246 | + File.write(fresh, "#{JSON.pretty_generate('sourceLanguage' => 'en', 'strings' => {}, 'version' => '1.0')}\n") |
| 247 | + stringsdata = stringsdata_files(stringsdata_dir) |
| 248 | + UI.user_error!('xcstringstool produced no .stringsdata for the App Intents sources') if stringsdata.empty? |
| 249 | + sh('xcrun', 'xcstringstool', 'sync', fresh, *stringsdata.flat_map { |f| ['--stringsdata', f] }) |
| 250 | + |
| 251 | + JSON.parse(File.read(fresh))['strings'].each_with_object({}) do |(key, entry), acc| |
| 252 | + value = entry.dig('localizations', 'en', 'stringUnit', 'value') |
| 253 | + acc[key] = { value: positionalize_untyped_arguments(value), comment: entry['comment'] } unless value.nil? |
| 254 | + end |
| 255 | + end |
| 256 | + end |
| 257 | + |
| 258 | + # The build-free extraction cannot type interpolations (a defaultValue's `\(…)` segments), so it |
| 259 | + # emits untyped `%arg` placeholders. Rewrite them as positional printf specifiers (`%1$@`, `%2$@`, |
| 260 | + # …), which is what GlotPress translators and the runtime's format-style resolution expect. This is |
| 261 | + # only correct for String-valued interpolations, so App Intents defaultValues must interpolate |
| 262 | + # preformatted Strings, never raw numbers or dates (see docs/localization.md). |
| 263 | + def positionalize_untyped_arguments(value) |
| 264 | + index = 0 |
| 265 | + value.gsub(/%(\d+\$)?arg/) do |
| 266 | + index += 1 |
| 267 | + "%#{Regexp.last_match(1) || "#{index}$"}@" |
| 268 | + end |
| 269 | + end |
203 | 270 | end |
0 commit comments