Skip to content

Commit 9a69539

Browse files
committed
Warn when stringsdict→pot drops plural forms other than one/other
The forward conversion only maps `one`/`other` onto gettext `msgid`/`msgid_plural`. Any other CLDR category in the English source — most commonly a `zero` literal override, which iOS honors even for English (Apple's docs return "No homes found" for 0) — has no gettext slot and was dropped from the `.pot` silently, so it never reached the translation system and vanished from every localized build. gettext/GlotPress can't carry a literal-0 override as a plural form (a 2-form locale has nowhere to put it), so the right home for such count-specific messages is a dedicated string selected in code (`if count == 0`), not a `zero` key in a `.stringsdict` bound for this pipeline. Rather than carry it through, surface it: `build_pot_entry` now logs a `UI.important` warning naming the entry, variable, and dropped categories, and the action docs state the limitation. The converted output is unchanged.
1 parent fe9b425 commit 9a69539

4 files changed

Lines changed: 73 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ _None_
1111
### New Features
1212

1313
- Added `find_or_create_pull_request` action and `GithubHelper#find_pull_request`: returns the URL of the open Pull Request for a head branch, creating one only if none exists yet. Useful for "rolling" automations (e.g. a daily translations or dependency-update job) that force-push the same head branch on every run. [#733]
14-
- Added `ios_generate_pot_from_stringsdict` and `ios_generate_stringsdict_from_po` actions to round-trip iOS `.stringsdict` plural files through a gettext `.po`/`.pot`-based translation system (e.g. GlotPress). The forward action turns an English `.stringsdict` into a `.pot` template (one `msgid`/`msgid_plural` per plural variable); the reverse rebuilds a per-locale `.stringsdict` from a translated `.po`, mapping gettext's indexed plural forms back to CLDR plural categories (`one`/`few`/`many`/…) via the new `Ios::PluralRules` table and using the English `.stringsdict` as a structural template. [#739]
14+
- Added `ios_generate_pot_from_stringsdict` and `ios_generate_stringsdict_from_po` actions to round-trip iOS `.stringsdict` plural files through a gettext `.po`/`.pot`-based translation system (e.g. GlotPress). The forward action turns an English `.stringsdict` into a `.pot` template (one `msgid`/`msgid_plural` per plural variable); the reverse rebuilds a per-locale `.stringsdict` from a translated `.po`, mapping gettext's indexed plural forms back to CLDR plural categories (`one`/`few`/`many`/…) via the new `Ios::PluralRules` table and using the English `.stringsdict` as a structural template. The forward action round-trips only the `one`/`other` forms; other CLDR categories in the source (e.g. a `zero` literal override) are dropped with a warning. [#739]
1515

1616
### Bug Fixes
1717

lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_generate_pot_from_stringsdict.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@ def self.details
3737
are keyed by a deterministic `msgctxt` (the `.stringsdict` key for single-variable
3838
entries, or `key:variable` for entries that reference multiple plural variables).
3939
40+
Only the `one` and `other` forms are converted. Any other CLDR category in the
41+
source — including an explicit `zero` literal override (e.g. "No items" for a
42+
count of 0) — has no gettext equivalent, so it is dropped from the `.pot` and a
43+
warning is logged. Handle such count-specific messages as dedicated strings
44+
selected in code (e.g. `if count == 0`) rather than as `zero`/`two`/… keys in a
45+
`.stringsdict` bound for this pipeline.
46+
4047
Use `ios_generate_stringsdict_from_po` to convert the translated `.po` files back
4148
into per-locale `.stringsdict` files once translation is complete.
4249
DETAILS

lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_stringsdict_helper.rb

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ module Ios
1717
# **Forward** ({generate_pot}): an English `.stringsdict` becomes a `.pot`
1818
# template. Each plural variable becomes one `msgid`/`msgid_plural` entry
1919
# (English `one` → `msgid`, English `other` → `msgid_plural`), keyed by a
20-
# deterministic `msgctxt`.
20+
# deterministic `msgctxt`. Only `one`/`other` map to gettext; any other CLDR
21+
# category present in the source — most notably an explicit `zero` literal
22+
# override (which iOS honors even for English, e.g. "No items" for a count
23+
# of 0) — has no gettext slot and is dropped from the `.pot` with a warning.
2124
#
2225
# **Reverse** ({generate_stringsdict_from_po}): a translated `.po` for a
2326
# locale, plus the original English `.stringsdict` as a structural template,
@@ -100,7 +103,7 @@ def self.generate_pot(stringsdict_paths:, output_path:)
100103
"`#{seen_contexts[context]}`). Stringsdict keys must be unique across the files being converted."
101104
end
102105
seen_contexts[context] = path
103-
entries << build_pot_entry(context: context, var_dict: var_dict, key: key)
106+
entries << build_pot_entry(context: context, var_dict: var_dict, key: key, variable: var_name)
104107
end
105108
end
106109
end
@@ -197,14 +200,16 @@ def self.context_for(key:, variable:, single_variable:)
197200
single_variable ? key : "#{key}:#{variable}"
198201
end
199202

200-
def self.build_pot_entry(context:, var_dict:, key:)
203+
def self.build_pot_entry(context:, var_dict:, key:, variable:)
201204
singular = var_dict['one'] || var_dict['other']
202205
plural = var_dict['other']
203206
if singular.nil? || plural.nil?
204207
raise "Stringsdict entry '#{key}' is missing a required plural form " \
205208
"(needs at least 'other'; 'one' recommended for the singular)."
206209
end
207210

211+
warn_dropped_categories(key: key, variable: variable, var_dict: var_dict)
212+
208213
entry = GetText::POEntry.new(:msgctxt_plural)
209214
entry.msgctxt = context
210215
entry.msgid = singular
@@ -214,6 +219,26 @@ def self.build_pot_entry(context:, var_dict:, key:)
214219
entry
215220
end
216221

222+
# Only `one`/`other` survive the gettext round-trip (as `msgid`/
223+
# `msgid_plural`). Any other CLDR category in the source variable — most
224+
# commonly a `zero` literal override, which iOS honors even for English
225+
# (Apple's docs: an English `zero` returns "No homes found" for `0`) — has
226+
# no gettext equivalent and is dropped from the `.pot`. Warn so it isn't
227+
# lost silently.
228+
def self.warn_dropped_categories(key:, variable:, var_dict:)
229+
dropped = (CLDR_CATEGORIES - %w[one other]) & var_dict.keys
230+
return if dropped.empty?
231+
232+
UI.important(
233+
"Stringsdict entry '#{key}' (variable '#{variable}') has plural form(s) " \
234+
"[#{dropped.join(', ')}] that gettext can't represent — they will be dropped " \
235+
"from the `.pot` and won't be translated. Only 'one' and 'other' round-trip; " \
236+
"handle a count-specific message (e.g. a 'zero'/empty-state string) as a dedicated " \
237+
'string selected in code (`if count == 0`), not as a plural key in a `.stringsdict` ' \
238+
'bound for this pipeline.'
239+
)
240+
end
241+
217242
# The translated plural forms for a context, or `[]` if untranslated.
218243
def self.translated_forms(parsed_po:, context:, source_var:)
219244
msgid = source_var['one'] || source_var['other']

spec/ios_stringsdict_helper_spec.rb

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,43 @@ def convert_po_to_stringsdict(po_content, template:, locale:)
132132
.to raise_error(/missing a required plural form/)
133133
end
134134
end
135+
136+
it 'warns and drops source plural forms other than one/other (e.g. a `zero` override)' do
137+
in_tmp_dir do |dir|
138+
src = File.join(dir, 'zero.stringsdict')
139+
described_class.write(
140+
data: {
141+
'homes' => {
142+
'NSStringLocalizedFormatKey' => '%#@count@',
143+
'count' => {
144+
'NSStringFormatSpecTypeKey' => 'NSStringPluralRuleType',
145+
# iOS honors an English `zero` override (Apple: returns "No homes found" for 0),
146+
# but gettext has no slot for it — so it must be dropped *loudly*.
147+
'zero' => 'No homes found',
148+
'one' => '%d home found',
149+
'other' => '%d homes found'
150+
}
151+
}
152+
},
153+
path: src
154+
)
155+
pot = File.join(dir, 'out.pot')
156+
157+
expect(FastlaneCore::UI).to receive(:important) do |message|
158+
expect(message).to include('zero')
159+
expect(message).to match(/drop/i)
160+
end
161+
162+
described_class.generate_pot(stringsdict_paths: src, output_path: pot)
163+
164+
content = File.read(pot)
165+
# `one`/`other` still convert…
166+
expect(content).to include('msgid "%d home found"')
167+
expect(content).to include('msgid_plural "%d homes found"')
168+
# …but the `zero` literal override is dropped.
169+
expect(content).not_to include('No homes found')
170+
end
171+
end
135172
end
136173

137174
describe '.generate_stringsdict_from_po' do

0 commit comments

Comments
 (0)