Skip to content

Commit d25fd59

Browse files
committed
Add linter to ensure social media callouts are within the message limits
1 parent ed12588 commit d25fd59

16 files changed

Lines changed: 364 additions & 270 deletions

File tree

lint_yaml.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
This catches the common mistake of converting a YAML quoted string
1515
to a block scalar without removing the outer quotes (which become
1616
literal characters in block scalars).
17+
5. social_callout_{platform}_{n} values fit within each platform's
18+
post character limit after {url} substitution, and contain the
19+
required {url} placeholder so the share intent gets a link.
1720
"""
1821

1922
import glob
@@ -209,6 +212,78 @@ def check_quoted_block_scalars(path):
209212
return errors
210213

211214

215+
# Per-platform character limits for social_callout_{platform}_{n} messages.
216+
# url_weight = 23 reflects platforms that fold every URL to a fixed width
217+
# (X via t.co, Mastodon per its character-counting rules). Other platforms
218+
# count the URL as its visible length, so we substitute the longest URL
219+
# the site ever emits: https://keepandroidopen.org/{locale}/ — pt-BR and
220+
# zh-CN are tied at 34 chars.
221+
SOCIAL_PLATFORM_LIMITS = {
222+
"x": {"limit": 280, "url_weight": 23},
223+
"bluesky": {"limit": 300, "url_weight": None},
224+
"mastodon": {"limit": 500, "url_weight": 23},
225+
"linkedin": {"limit": 3000, "url_weight": None},
226+
"facebook": {"limit": 63206, "url_weight": None},
227+
}
228+
LONGEST_SUBSTITUTED_URL = "https://keepandroidopen.org/zh-CN/"
229+
230+
_SOCIAL_CALLOUT_KEY = re.compile(r"^social_callout_([a-z]+)_(\d+)$")
231+
232+
233+
def check_social_callout_limits(path):
234+
"""Return a list of error strings for social_callout_* keys whose
235+
rendered length exceeds the target platform's post character limit,
236+
or that are missing the required {url} placeholder.
237+
238+
Only applies to src/i18n/locales/*.yaml.
239+
"""
240+
if "/locales/" not in path:
241+
return []
242+
243+
errors = []
244+
try:
245+
with open(path) as fh:
246+
data = yaml.safe_load(fh)
247+
except yaml.YAMLError:
248+
return [] # syntax error already reported
249+
250+
if not isinstance(data, dict):
251+
return []
252+
253+
for key, value in data.items():
254+
m = _SOCIAL_CALLOUT_KEY.match(key)
255+
if not m:
256+
continue
257+
platform = m.group(1)
258+
cfg = SOCIAL_PLATFORM_LIMITS.get(platform)
259+
if cfg is None:
260+
continue
261+
if not isinstance(value, str):
262+
errors.append(f"{path}: key '{key}': expected string value")
263+
continue
264+
265+
if "{url}" not in value:
266+
errors.append(
267+
f"{path}: key '{key}': missing required {{url}} placeholder"
268+
)
269+
continue
270+
271+
if cfg["url_weight"] is not None:
272+
rendered = value.replace("{url}", "x" * cfg["url_weight"])
273+
else:
274+
rendered = value.replace("{url}", LONGEST_SUBSTITUTED_URL)
275+
276+
n = len(rendered)
277+
if n > cfg["limit"]:
278+
errors.append(
279+
f"{path}: key '{key}': {n} chars exceeds {platform} "
280+
f"limit of {cfg['limit']} (counted after {{url}} "
281+
f"substitution)"
282+
)
283+
284+
return errors
285+
286+
212287
def main():
213288
files = find_yaml_files()
214289
if not files:
@@ -221,6 +296,7 @@ def main():
221296
all_errors.extend(check_escaped_quotes_in_block_scalars(path))
222297
all_errors.extend(check_html_in_locale_values(path))
223298
all_errors.extend(check_quoted_block_scalars(path))
299+
all_errors.extend(check_social_callout_limits(path))
224300

225301
if all_errors:
226302
print(f"Found {len(all_errors)} error(s):\n")

0 commit comments

Comments
 (0)