Skip to content

feat: add plural translations - #20

Closed
HarshMN2345 wants to merge 3 commits into
mainfrom
feat-plurals
Closed

HarshMN2345 wants to merge 3 commits into
mainfrom
feat-plurals

Conversation

@HarshMN2345

Copy link
Copy Markdown
Member

What does this PR do?

Adds plural translations, so Appwrite emails can show a relative expiry like "This code will expire in 15 minutes" with the correct plural form in every language (appwrite/appwrite#13726).

  • getPlural(string $key, int $count, ?string $default) formats a translation written as an ICU MessageFormat pattern with a count argument, e.g. {count, plural, one {через # минуту} few {через # минуты} many {через # минут} other {через # минуты}}. It uses the CLDR plural rules of the language that has the translation, and falls back to the fallback language when the key is missing or the pattern doesn't parse.
  • getText() takes plurals: ['expire' => ['emails.expire.minutes', 15]]. Each plural placeholder is formatted in the same language as the text it fills, so an English fallback sentence doesn't get a Russian phrase.
  • setLanguageFromArray() and setLanguageFromJSON() take an optional locale whose plural rules apply. Without it, a code that loads another language's file would pick the wrong form (Serbian rules on English text render "in 21 minute").

Plurals need ext-intl, added as a suggestion. The rules come from ICU, which ships them for every language even with English-only ICU data. Existing methods are unchanged.

Test Plan

  • composer lint, composer check (PHPStan max) and composer test pass.
  • Ran against PHP 8.5 with ICU 78.1 (English-only data):
    • ru 1/2/5/11/21/22 → через 1 минуту / 2 минуты / 5 минут / 11 минут / 21 минуту / 22 минуты
    • Missing ru key or broken ru pattern → en fallback with en rules
    • Missing key → {{key}}, the given default, or an exception when exceptions are on
    • setLanguageFromJSON('sr', 'en.json', 'en') → "in 21 minutes"; without the rules locale → "in 21 minute"

Format ICU MessageFormat plural translations with the CLDR rules of the
language that has the translation. getPlural returns a plural
translation, getText fills plural placeholders in the language of the
text it returns, and languages that load another language's
translations can name the locale whose plural rules apply.
@greptile-apps

greptile-apps Bot commented Sep 17, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

The PR appears safe to merge; the previously missing behavioral coverage has been added, and the resolved implementation-coupled assertion has been removed.

Fix All in Claude CodeFindings

  1. P2 Plural Behavior Lacks Tests ▶
Fix with agent prompt
### Issue 1
src/Locale/Locale.php:190-193
This locale-sensitive feature has no automated behavioral coverage. Add observable tests for `getPlural()` and plural placeholders that cover CLDR category selection, fallback-language rule selection, malformed patterns, missing keys and defaults, and exception mode. Without these tests, the feature's core behavior can regress while the existing suite remains green.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

Adds ICU MessageFormat-based plural translations with locale-specific CLDR rules, fallback-language formatting, custom arguments, and plural placeholders in ordinary translations.

  • Adds locale-rule tracking to array and JSON language registration.
  • Requires PHP 8.3 and ext-intl, including CI installation.
  • Documents plural translation patterns and fallback behavior.
  • Adds observable tests covering plural categories, fractional counts, locale overrides, fallbacks, malformed patterns, defaults, and exception mode.
  • Removes the implementation-coupled language-registration count assertion identified previously.

Reviews (3) · Last reviewed commit: "test: drop the registered language count..."

Comment thread src/Locale/Locale.php Outdated
Comment on lines +190 to +193
public function getPlural(string $key, int $count, string|null $default = self::DEFAULT_DYNAMIC_KEY): ?string
{
return $this->format($this->default, $key, $count) ?? ($default === self::DEFAULT_DYNAMIC_KEY ? '{{'.$key.'}}' : $default);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Plural Behavior Lacks Tests

This locale-sensitive feature has no automated behavioral coverage. Add observable tests for getPlural() and plural placeholders that cover CLDR category selection, fallback-language rule selection, malformed patterns, missing keys and defaults, and exception mode. Without these tests, the feature's core behavior can regress while the existing suite remains green.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/Locale/Locale.php
Line: 190-193

Comment:
**Plural Behavior Lacks Tests**

This locale-sensitive feature has no automated behavioral coverage. Add observable tests for `getPlural()` and plural placeholders that cover CLDR category selection, fallback-language rule selection, malformed patterns, missing keys and defaults, and exception mode. Without these tests, the feature's core behavior can regress while the existing suite remains green.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code Fix in Codex

Run the suite with the intl extension so the plural paths are exercised,
and cover CLDR category selection, the plural rules of a language that
loads another language's translations, fallback formatting, invalid
patterns and defaults.

Plural counts accept floats, since CLDR gives fractions their own
category, and patterns can take other ICU arguments. An invalid pattern
now reports itself instead of looking like a missing translation.

The intl extension moves to require: it is needed by a method on the
only class this library has. The PHP constraint follows the typed class
constant the code already uses.
Comment thread tests/Locale/LocaleTest.php Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant