Skip to content

Feat: AI locale translations #10322

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: 1.7.x
Choose a base branch
from
Open

Feat: AI locale translations #10322

wants to merge 5 commits into from

Conversation

Meldiron
Copy link
Contributor

What does this PR do?

Adds script to add missing translations in all locales

Test Plan

Run manually, fill already missing keys

Related PRs and Issues

x

Checklist

  • Have you read the Contributing Guidelines on issues?
  • If the PR includes a change to an API's metadata (desc, label, params, etc.), does it also include updated API specs and example docs?

Copy link
Contributor

coderabbitai bot commented Aug 14, 2025

📝 Walkthrough

Walkthrough

Adds large translation blocks to de.json, he.json, ne.json, nl.json, pt-br.json, and pt-pt.json under app/config/locale/translations (emails., settings., countries., continents., and related keys). Adds a new CLI task class DevGenerateTranslations at src/Appwrite/Platform/Tasks/DevGenerateTranslations.php that uses an OpenAI agent to generate missing translations, with --dry-run, --api-key, and --enforced-keys options. Registers the task in src/Appwrite/Platform/Services/Tasks.php. Adds bin/dev-generate-translations wrapper script and makes it executable in the Dockerfile. Adds composer dependency utopia-php/agents ^0.4.4.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • 1.7.x #9897 — Modifies the same locale translation files (de.json, he.json, ne.json, nl.json, pt-br.json, pt-pt.json); strongly related to the translation additions in this PR.

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat-ai-translations

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

github-actions bot commented Aug 14, 2025

Security Scan Results for PR

Docker Image Scan Results

Package Version Vulnerability Severity
binutils 2.42-r0 CVE-2025-0840 HIGH
git 2.45.3-r0 CVE-2025-48384 HIGH
git 2.45.3-r0 CVE-2025-48385 HIGH
git-init-template 2.45.3-r0 CVE-2025-48384 HIGH
git-init-template 2.45.3-r0 CVE-2025-48385 HIGH
icu 74.2-r0 CVE-2025-5222 HIGH
icu-data-en 74.2-r0 CVE-2025-5222 HIGH
icu-dev 74.2-r0 CVE-2025-5222 HIGH
icu-libs 74.2-r0 CVE-2025-5222 HIGH
libexpat 2.6.4-r0 CVE-2024-8176 HIGH
libxml2 2.12.7-r0 CVE-2024-56171 HIGH
libxml2 2.12.7-r0 CVE-2025-24928 HIGH
libxml2 2.12.7-r0 CVE-2025-27113 HIGH
libxml2 2.12.7-r0 CVE-2025-32414 HIGH
libxml2 2.12.7-r0 CVE-2025-32415 HIGH
pyc 3.12.9-r0 CVE-2024-12718 HIGH
pyc 3.12.9-r0 CVE-2025-4138 HIGH
pyc 3.12.9-r0 CVE-2025-4330 HIGH
pyc 3.12.9-r0 CVE-2025-4517 HIGH
python3 3.12.9-r0 CVE-2024-12718 HIGH
python3 3.12.9-r0 CVE-2025-4138 HIGH
python3 3.12.9-r0 CVE-2025-4330 HIGH
python3 3.12.9-r0 CVE-2025-4517 HIGH
python3-pyc 3.12.9-r0 CVE-2024-12718 HIGH
python3-pyc 3.12.9-r0 CVE-2025-4138 HIGH
python3-pyc 3.12.9-r0 CVE-2025-4330 HIGH
python3-pyc 3.12.9-r0 CVE-2025-4517 HIGH
python3-pycache-pyc0 3.12.9-r0 CVE-2024-12718 HIGH
python3-pycache-pyc0 3.12.9-r0 CVE-2025-4138 HIGH
python3-pycache-pyc0 3.12.9-r0 CVE-2025-4330 HIGH
python3-pycache-pyc0 3.12.9-r0 CVE-2025-4517 HIGH
sqlite-libs 3.45.3-r1 CVE-2025-29087 HIGH
xz 5.6.2-r0 CVE-2025-31115 HIGH
xz-libs 5.6.2-r0 CVE-2025-31115 HIGH
golang.org/x/crypto v0.31.0 CVE-2025-22869 HIGH
golang.org/x/oauth2 v0.24.0 CVE-2025-22868 HIGH
stdlib 1.22.10 CVE-2025-47907 HIGH

Source Code Scan Results

🎉 No vulnerabilities found!

Copy link

✨ Benchmark results

  • Requests per second: 959
  • Requests with 200 status code: 172,651
  • P99 latency: 0.189959707

⚡ Benchmark Comparison

Metric This PR Latest version
RPS 959 965
200 172,651 173,812
P99 0.189959707 0.201635368

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 15

🔭 Outside diff range comments (13)
app/config/locale/translations/de.json (3)

53-55: German greeting in invitation email is in English

"Hello," should be "Hallo,".

Apply:

-    "emails.invitation.hello": "Hello,",
+    "emails.invitation.hello": "Hallo,",

84-86: Typo: “Brazilen” → “Brasilien”

Correct the country name spelling.

-    "countries.br": "Brazilen",
+    "countries.br": "Brasilien",

147-149: Country name in German

Use “Jordanien” instead of “Jordan”.

-    "countries.jo": "Jordan",
+    "countries.jo": "Jordanien",
app/config/locale/translations/he.json (3)

46-46: Spelling and word order: “סיסמא/סיסמה” and phrasing

Use the normative spelling “סיסמה” and fix button text order to “איפוס סיסמה”.

-    "emails.recovery.subject": "איפוס סיסמא",
+    "emails.recovery.subject": "איפוס סיסמה",
-    "emails.recovery.buttonText": "סיסמא איפוס",
+    "emails.recovery.buttonText": "איפוס סיסמה",

Also applies to: 51-51


91-91: Misspelling: Switzerland

Preferred spelling is “שווייץ”.

-    "countries.ch": "שוויץ",
+    "countries.ch": "שווייץ",

1-278: Fix missing placeholders in he.json and de.json

I ran a placeholder comparison against en.json — there are 5 placeholder mismatches in each of the Hebrew and German files. Please restore the missing moustache placeholders (exactly as in en.json).

Files / keys to fix:

  • app/config/locale/translations/he.json and app/config/locale/translations/de.json
    • emails.invitation.hello — expected {{user}} (missing)
    • emails.magicSession.hello — expected {{user}} (missing)
    • emails.magicSession.subject — expected {{project}} (missing)
    • emails.otpSession.hello — expected {{user}} (missing)
    • emails.verification.body — expected {{project}} (missing)

Action: update the translations to include the exact placeholders from en.json (including braces), then re-run the placeholder-check script to confirm no mismatches remain.

app/config/locale/translations/nl.json (2)

267-275: Inconsistent placeholder formatting: missing {{b}}...{{/b}} wrappers

Other locales and related keys (e.g., emails.mfaChallenge.clientInfo) use {{b}} wrappers to bold dynamic values. These two lines lack them, leading to inconsistent markup in emails.

Apply this diff:

-    "emails.magicSession.clientInfo": "Deze aanmelding is aangevraagd met {{agentClient}} op {{agentDevice}} {{agentOs}}. Als u de aanmelding niet heeft aangevraagd, kunt u deze e-mail gerust negeren.",
+    "emails.magicSession.clientInfo": "Deze aanmelding is aangevraagd met {{b}}{{agentClient}}{{/b}} op {{b}}{{agentDevice}}{{/b}} {{b}}{{agentOs}}{{/b}}. Als u de aanmelding niet heeft aangevraagd, kunt u deze e-mail gerust negeren.",
@@
-    "emails.otpSession.clientInfo": "Deze aanmelding is aangevraagd met {{agentClient}} op {{agentDevice}} {{agentOs}}. Als u de aanmelding niet heeft aangevraagd, kunt u deze e-mail veilig negeren.",
+    "emails.otpSession.clientInfo": "Deze aanmelding is aangevraagd met {{b}}{{agentClient}}{{/b}} op {{b}}{{agentDevice}}{{/b}} {{b}}{{agentOs}}{{/b}}. Als u de aanmelding niet heeft aangevraagd, kunt u deze e-mail veilig negeren.",

100-110: Fix Dutch country names in app/config/locale/translations/nl.json

Several country names are in English or have spelling/formatting issues. Verified keys and recommended corrections:

  • countries.cv: "Cape Verde" -> "Kaapverdië"
  • countries.do: "Dominican Republic" -> "Dominicaanse Republiek"
  • countries.gq: "Equatorial Guinea" -> "Equatoriaal-Guinea"
  • countries.hr: "Croatië" -> "Kroatië"
  • countries.il: "Israel" -> "Israël"
  • countries.kh: "Cambodia" -> "Cambodja"
  • countries.kr: "Zuid Korea" -> "Zuid-Korea"
  • countries.kp: "Noord Korea" -> "Noord-Korea"
  • countries.ly: "Libya" -> "Libië"
  • countries.ss: "Zuid Soedan" -> "Zuid-Soedan"
  • countries.se: "Sweden" -> "Zweden"
  • countries.tr: "Turkijë" -> "Turkije"

Proposed diff:

-    "countries.cv": "Cape Verde",
+    "countries.cv": "Kaapverdië",
-    "countries.do": "Dominican Republic",
+    "countries.do": "Dominicaanse Republiek",
-    "countries.gq": "Equatorial Guinea",
+    "countries.gq": "Equatoriaal-Guinea",
-    "countries.hr": "Croatië",
+    "countries.hr": "Kroatië",
-    "countries.il": "Israel",
+    "countries.il": "Israël",
-    "countries.kh": "Cambodia",
+    "countries.kh": "Cambodja",
-    "countries.kr": "Zuid Korea",
+    "countries.kr": "Zuid-Korea",
-    "countries.kp": "Noord Korea",
+    "countries.kp": "Noord-Korea",
-    "countries.ly": "Libya",
+    "countries.ly": "Libië",
-    "countries.ss": "Zuid Soedan",
+    "countries.ss": "Zuid-Soedan",
-    "countries.se": "Sweden",
+    "countries.se": "Zweden",
-    "countries.tr": "Turkijë",
+    "countries.tr": "Turkije",

I can run a consistency check across all locale files to ensure translations are uniform if you want.

app/config/locale/translations/pt-pt.json (1)

104-105: PT-PT translations — replace PT-BR spellings with European Portuguese forms

Verified (Priberam / EU / MNE-PT): use Chéquia, Irão, Roménia, São Vicente.

Files/locations to change:

  • app/config/locale/translations/pt-pt.json — lines 104 (countries.cz), 141 (countries.ir), 208 (countries.ro), 249 (countries.vc)

Suggested diff:

-    "countries.cz": "Tchéquia",
+    "countries.cz": "Chéquia",
-    "countries.ir": "Irã",
+    "countries.ir": "Irão",
-    "countries.ro": "Romênia",
+    "countries.ro": "Roménia",
-    "countries.vc": "São Vincente e Granadinas",
+    "countries.vc": "São Vicente e Granadinas",
app/config/locale/translations/ne.json (4)

266-266: French text in Nepali locale: magicSession.buttonText

The button label is in French.

-    "emails.magicSession.buttonText": "Connectez-vous à {{project}}",
+    "emails.magicSession.buttonText": "{{project}} मा साइन इन गर्नुहोस्",

35-35: Inconsistent “sender/signature” nouns across emails (समूह / टोली / टिम)

Use one consistent term across the file. Early lines use “टोली”; adopt that everywhere for consistency.

-    "emails.sender": "%s समूह",
+    "emails.sender": "%s टोली",
-    "emails.verification.signature": "{{project}} समूह",
+    "emails.verification.signature": "{{project}} टोली",
-    "emails.magicSession.signature": "{{project}} समूह",
+    "emails.magicSession.signature": "{{project}} टोली",
-    "emails.recovery.signature": "{{project}} समूह",
+    "emails.recovery.signature": "{{project}} टोली",
-    "emails.invitation.signature": "{{project}} समूह",
+    "emails.invitation.signature": "{{project}} टोली",
-    "emails.otpSession.signature": "{{project}} टिम"
+    "emails.otpSession.signature": "{{project}} टोली"

Also applies to: 41-41, 45-45, 52-52, 59-59, 277-277


97-97: Incorrect country names/transliterations for several ISO codes

These will surface incorrect UI labels. Suggested corrections:

-    "countries.cd": "दि आर कांगो",
+    "countries.cd": "डीआर कंगो",
-    "countries.ga": "गेबन",
+    "countries.ga": "गाबोन",
-    "countries.gn": "गुयना",
+    "countries.gn": "गिनी",
-    "countries.gw": "गुयना-बिसाउ",
+    "countries.gw": "गिनी-बिसाउ",
-    "countries.gq": "इक्वेटोरियल गुयना",
+    "countries.gq": "इक्वेटोरियल गिनी",
-    "countries.mc": "मोरक्को",
+    "countries.mc": "मोनाको",
-    "countries.pg": "पापुआ न्यू गुयना",
+    "countries.pg": "पापुआ न्यु गिनी",
-    "countries.us": "युनाइटेड अमेरिका",
+    "countries.us": "संयुक्त राज्य अमेरिका",

If you want, I can cross-check the full country list against authoritative Nepali names and supply a complete patch.

Also applies to: 122-122, 125-125, 128-129, 170-170, 203-203, 246-246


268-268: Add a short label to SMS verification messages in en.json and ne.json

Bare "{{secret}}" provides poor UX and can trigger anti‑spam heuristics — make the SMS a short labelled sentence and keep locales consistent. Confirm final copy with product and SMS length constraints.

Files to update:

  • app/config/locale/translations/en.json — line ~72
  • app/config/locale/translations/ne.json — line ~268

Suggested diffs:

app/config/locale/translations/en.json

-    "sms.verification.body": "{{secret}}",
+    "sms.verification.body": "Your verification code: {{secret}}",

app/config/locale/translations/ne.json

-    "sms.verification.body": "{{secret}}",
+    "sms.verification.body": "तपाईंको प्रमाणिकरण कोड: {{secret}}",

Please adjust the exact phrasing to match product copy and SMS character limits.

🧹 Nitpick comments (17)
app/config/locale/translations/de.json (4)

227-228: Outdated country name: Swasiland → Eswatini

Modern name is “Eswatini”.

-    "countries.sz": "Swasiland",
+    "countries.sz": "Eswatini",

26-26: Trailing space in greeting

Trailing whitespace in localized strings can look odd in UI.

-    "emails.sessionAlert.hello": "Hallo {{user}}, ",
+    "emails.sessionAlert.hello": "Hallo {{user}},",

2-2: Unify “Team” signature style

File mixes “{{project}} Team” and “{{project}}-Team”. Align to one style (rest of file largely uses “-Team”).

-    "emails.certificate.signature": "{{project}} Team",
+    "emails.certificate.signature": "{{project}}-Team",
-    "emails.mfaChallenge.signature": "{{project}} Team",
+    "emails.mfaChallenge.signature": "{{project}}-Team",
-    "emails.sessionAlert.signature": "{{project}} Team",
+    "emails.sessionAlert.signature": "{{project}}-Team",
-    "emails.otpSession.signature": "{{project}} Team",
+    "emails.otpSession.signature": "{{project}}-Team",

Also applies to: 11-11, 19-19, 277-277


278-279: Confirm the “mock” key is intentional

"mock" looks like a test stub. If unused in production, consider removing to avoid shipping dead keys.

I can scan for references to the "mock" key across the repo and open a cleanup PR if it’s unused.

app/config/locale/translations/he.json (2)

12-12: Unnatural phrasing for thanks

Prefer common "תודה," over "מודה לך,".

-    "emails.mfaChallenge.thanks": "מודה לך,",
+    "emails.mfaChallenge.thanks": "תודה,",

17-17: Inconsistent punctuation in subject

Avoid trailing period in subject for consistency with other locales.

-    "emails.mfaChallenge.subject": "קוד האימות עבור {{project}}.",
+    "emails.mfaChallenge.subject": "קוד האימות עבור {{project}}",
src/Appwrite/Platform/Tasks/DevGenerateTranslations.php (2)

31-36: Parameter typing and dry-run parsing

Param uses Boolean validator, but action receives mixed and then checks only for string 'true'. Support common truthy inputs and type to bool locally.

-    public function action(mixed $dryRun, string $apiKey): void
+    public function action(mixed $dryRun, string $apiKey): void
     {
-        $dryRun = \strval($dryRun) === 'true';
+        $value = \strtolower(\strval($dryRun));
+        $dryRun = \in_array($value, ['1', 'true', 'on', 'yes'], true);

Also applies to: 38-46


83-94: Locale language hint can be ambiguous (e.g., pt-br vs pt-pt)

Passing “pt-br”/“pt-pt” raw in the prompt may degrade quality. Map to human-friendly hints like “Portuguese (Brazil)” and “Portuguese (Portugal)”.

I can add a simple mapping:

  • pt-br => Portuguese (Brazil)
  • pt-pt => Portuguese (Portugal)
  • zh-cn/zh-tw, etc.

Want me to push a patch with a small locale-to-language map?

app/config/locale/translations/nl.json (2)

37-38: Typo: “verifieren” → “verifiëren”

Correct Dutch spelling with trema.

-    "emails.verification.body": "Volg deze link om uw e-mail te verifieren",
+    "emails.verification.body": "Volg deze link om uw e-mail te verifiëren",

55-56: Typos in invitation email

Fix past participle and diacritics.

-    "emails.invitation.body": "U ontvangt deze mail want u was uitgenodig door {{owner}} om lid van het {{team}} team te worden in {{project}} ",
-    "emails.invitation.footer": "Als u niet geintereseerd bent, kan u deze mail negeren.",
+    "emails.invitation.body": "U ontvangt deze mail omdat u bent uitgenodigd door {{owner}} om lid van het {{team}}-team te worden in {{project}}.",
+    "emails.invitation.footer": "Als u niet geïnteresseerd bent, kunt u deze mail negeren.",
app/config/locale/translations/pt-pt.json (4)

36-37: Greeting tone is not PT-PT

“Hey” is informal/English. Prefer “Olá”.

-    "emails.verification.hello": "Hey {{user}},",
+    "emails.verification.hello": "Olá {{user}},",

43-45: Trailing space after greeting

Minor cleanup.

-    "emails.magicSession.hello": "Olá ,",
+    "emails.magicSession.hello": "Olá,",

15-16: Superfluous newline in hello

Remove the newline escape.

-    "emails.mfaChallenge.hello": "Olá {{user}}, \n",
+    "emails.mfaChallenge.hello": "Olá {{user}},",

267-275: Add bold wrappers to match other templates

For consistency with mfaChallenge.clientInfo, wrap dynamic parts with {{b}} tags.

-    "emails.magicSession.clientInfo": "Este início de sessão foi solicitado usando o {{agentClient}} no {{agentDevice}} {{agentOs}}. Se não foi você quem solicitou o início de sessão, pode ignorar este e-mail com segurança.",
+    "emails.magicSession.clientInfo": "Este início de sessão foi solicitado usando o {{b}}{{agentClient}}{{/b}} no {{b}}{{agentDevice}}{{/b}} {{b}}{{agentOs}}{{/b}}. Se não foi você quem solicitou o início de sessão, pode ignorar este e-mail com segurança.",
app/config/locale/translations/pt-br.json (3)

26-27: Trailing space after greeting

Minor cleanup.

-    "emails.sessionAlert.hello": "Olá {{user}}, ",
+    "emails.sessionAlert.hello": "Olá {{user}},",

267-275: Normalize clientInfo placeholders to include bold wrappers

Keep formatting consistent across templates.

-    "emails.magicSession.clientInfo": "Este acesso foi solicitado usando {{agentClient}} em {{agentDevice}} {{agentOs}}. Se você não solicitou o acesso, pode ignorar este e-mail com segurança.",
+    "emails.magicSession.clientInfo": "Este acesso foi solicitado usando {{b}}{{agentClient}}{{/b}} em {{b}}{{agentDevice}}{{/b}} {{b}}{{agentOs}}{{/b}}. Se você não solicitou o acesso, pode ignorar este e-mail com segurança.",
@@
-    "emails.otpSession.clientInfo": "Este acesso foi solicitado usando {{agentClient}} em {{agentDevice}} {{agentOs}}. Se você não solicitou o acesso, pode ignorar este e-mail com segurança.",
+    "emails.otpSession.clientInfo": "Este acesso foi solicitado usando {{b}}{{agentClient}}{{/b}} em {{b}}{{agentDevice}}{{/b}} {{b}}{{agentOs}}{{/b}}. Se você não solicitou o acesso, pode ignorar este e-mail com segurança.",

276-277: Signature inconsistent with the rest of PT-BR (“Time” vs “equipe”)

Elsewhere you use “Time {{project}}”. Align this here.

-    "emails.otpSession.signature": "equipe {{project}}"
+    "emails.otpSession.signature": "Time {{project}}"
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these settings in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 405aa7f and f79d57e.

⛔ Files ignored due to path filters (1)
  • composer.lock is excluded by !**/*.lock
📒 Files selected for processing (11)
  • Dockerfile (1 hunks)
  • app/config/locale/translations/de.json (2 hunks)
  • app/config/locale/translations/he.json (2 hunks)
  • app/config/locale/translations/ne.json (2 hunks)
  • app/config/locale/translations/nl.json (2 hunks)
  • app/config/locale/translations/pt-br.json (2 hunks)
  • app/config/locale/translations/pt-pt.json (2 hunks)
  • bin/dev-generate-translations (1 hunks)
  • composer.json (1 hunks)
  • src/Appwrite/Platform/Services/Tasks.php (2 hunks)
  • src/Appwrite/Platform/Tasks/DevGenerateTranslations.php (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/Appwrite/Platform/Services/Tasks.php (1)
src/Appwrite/Platform/Tasks/DevGenerateTranslations.php (2)
  • DevGenerateTranslations (18-172)
  • getName (20-23)
🔇 Additional comments (6)
composer.json (1)

87-89: composer.lock already includes utopia-php/agents — no lockfile change needed

Verified: composer.json requires utopia-php/agents ^0.4.4 and composer.lock contains utopia-php/agents 0.4.4, so Docker composer install will include it.

  • composer.json (lines ~87–89): "utopia-php/agents": "^0.4.4"
  • composer.lock: utopia-php/agents 0.4.4
Dockerfile (1)

77-77: LGTM: Make wrapper executable

The new dev-generate-translations wrapper is correctly added to the executables block and will be runnable in the image.

src/Appwrite/Platform/Tasks/DevGenerateTranslations.php (1)

42-47: Non-dry run without API key: good check

Failing fast when apiKey is missing is correct.

src/Appwrite/Platform/Services/Tasks.php (1)

46-47: Task registration LGTM

New DevGenerateTranslations action is correctly imported and registered.

app/config/locale/translations/pt-br.json (1)

25-26: Escaped newline is double-escaped

You have “\n” (backslash-literal-n) embedded. If the intention is a real newline, use “\n” once. If literal “\n” is desired in the rendered email, leave as is.

Would you like to render a real newline in the email? If yes, change “\n” to “\n”.

app/config/locale/translations/ne.json (1)

7-8: Mixed placeholder syntaxes (Mustache vs sprintf) — confirmed expected engine per key

Short summary: I verified the code — subjects/preview for certificate and some subjects (e.g. invitation.subject) are formatted with sprintf (expect %s), while email bodies and many previews (e.g. invitation.preview) use Mustache ({{...}}) and are populated via setParam. This mixed usage is intentional and matches the current implementation.

Relevant evidence:

  • src/Appwrite/Platform/Workers/Certificates.php:398–399 — uses sprintf($locale->getText("emails.certificate.subject"), $domain) and sprintf(... "emails.certificate.preview" ...). => emails.certificate.subject / preview expect %s.
  • app/controllers/api/teams.php:660–661 — $preview = $locale->getText("emails.invitation.preview"); $subject = \sprintf($locale->getText("emails.invitation.subject"), $team->getAttribute('name'), $projectName); => invitation.subject expects %s, invitation.preview uses Mustache.
  • app/controllers/api/projects.php: ~2309–2323 — $message->setParam("{{{$param}}}", $localeObj->getText("emails.{$type}.{$param}"), ...) and setParam('{{hello}}', ...); 'subject' => $localeObj->getText('emails.' . $type . '.subject') => bodies use Mustache params; subjects are retrieved from translations and should be sprintf-formatted by callers when needed.
  • app/config/locale/translations/en.json:57–58 and 65–66 — invitation.subject / preview and certificate.subject / preview illustrate the %s vs {{...}} pattern.

Action: no change required — keep %s in translation keys that are sprintf-formatted (subjects / certificate previews) and keep Mustache placeholders for translation keys that are injected via setParam (bodies and certain previews).

"emails.mfaChallenge.preview": "השתמש בקוד {{otp}} לאימות דו-שלבי ב-{{project}}. פג תוקף תוך 15 דקות.",
"emails.mfaChallenge.subject": "קוד האימות עבור {{project}}.",
"emails.otpSession.preview": "השתמש ב-{{otp}} כדי להיכנס ל-{{project}}. פג תוקף בעוד 15 דקות.",
"emails.sessionAlert.signature": "מעבדת {{project}}",
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Incorrect term for “Team”

"מעבדת {{project}}" reads as “{{project}} Lab”. Should be “צוות {{project}}”.

-    "emails.sessionAlert.signature": "מעבדת {{project}}",
+    "emails.sessionAlert.signature": "צוות {{project}}",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"emails.sessionAlert.signature": "מעבדת {{project}}",
"emails.sessionAlert.signature": "צוות {{project}}",
🤖 Prompt for AI Agents
In app/config/locale/translations/he.json around line 19, the Hebrew translation
for "emails.sessionAlert.signature" incorrectly uses "מעבדת {{project}}" (means
"{{project}} Lab"); replace it with "צוות {{project}}" to correctly render "Team
{{project}}". Ensure spacing and placeholder {{project}} remain unchanged.

"emails.sessionAlert.hello": "שלום {{user}},",
"emails.sessionAlert.preview": "זוהתה התחברות חדשה ב-{{project}} בשעה {{time}} לפי זמן UTC.",
"emails.sessionAlert.subject": "התראת אבטחה: פתיחה של סשן חדש בחשבון {{project}} שלך",
"emails.magicSession.preview": "היכנס ל- {{project}} באמצעות הקישור המאובטח שלך. התוקף יפוג בעוד שעה אחת.",
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Unnecessary space after hyphen

Use “ל-{{project}}” without a space.

-    "emails.magicSession.preview": "היכנס ל- {{project}} באמצעות הקישור המאובטח שלך. התוקף יפוג בעוד שעה אחת.",
+    "emails.magicSession.preview": "היכנס ל-{{project}} באמצעות הקישור המאובטח שלך. התוקף יפוג בעוד שעה אחת.",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"emails.magicSession.preview": "היכנס ל- {{project}} באמצעות הקישור המאובטח שלך. התוקף יפוג בעוד שעה אחת.",
"emails.magicSession.preview": "היכנס ל-{{project}} באמצעות הקישור המאובטח שלך. התוקף יפוג בעוד שעה אחת.",
🤖 Prompt for AI Agents
In app/config/locale/translations/he.json around line 29, the Hebrew string
contains an unnecessary space after the hyphen: change "היכנס ל- {{project}}
באמצעות הקישור המאובטח שלך. התוקף יפוג בעוד שעה אחת." to remove the space so it
reads "היכנס ל-{{project}} באמצעות הקישור המאובטח שלך. התוקף יפוג בעוד שעה אחת."
ensuring no extra whitespace between the hyphen and the {{project}}
interpolation.

"emails.certificate.signature": "{{project}} टोली",
"emails.certificate.thanks": "धन्यवाद,",
"emails.certificate.footer": "तपाईंको अघिल्लो प्रमाणपत्र पहिलो विफलताबाट ३० दिनसम्म मान्य हुनेछ। हामी यो मामलाको जाँच गर्न अत्यधिक सिफारिस गर्दछौं, अन्यथा तपाईंको डोमेनको वैध SSL संचार बिना रहनेछ।",
"emails.certificate.body": "{{domain}}",
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Certificate body looks incomplete

Only "{{domain}}" is rendered. The English template usually contains a sentence around the domain. Recommend mirroring the full structure to avoid terse/odd emails.

I can update this line after we confirm the source text and placeholders.


🏁 Script executed:

#!/bin/bash
rg -n -A2 -B2 $'^\\s*"emails\\.certificate\\.body"\\s*:' app/config/locale/translations/en.json

Length of output: 680


Incomplete Nepali certificate email body — please mirror the English template including placeholders

ne.json currently contains only "{{domain}}"; the English source includes {{domain}}, {{attempt}} and {{error}}.

  • app/config/locale/translations/ne.json — line ~5 — current:
    • "emails.certificate.body": "{{domain}}",
  • app/config/locale/translations/en.json — line 68 — source:
    • "emails.certificate.body": "Certificate for your domain '{{domain}}' could not be generated. This is attempt no. {{attempt}}, and the failure was caused by: {{error}}"

Suggested replacement in ne.json (either translate the full sentence or copy the English placeholders until translation is provided):

-    "emails.certificate.body": "{{domain}}",
+    "emails.certificate.body": "Certificate for your domain '{{domain}}' could not be generated. This is attempt no. {{attempt}}, and the failure was caused by: {{error}}",
🤖 Prompt for AI Agents
In app/config/locale/translations/ne.json around line 5 the
"emails.certificate.body" value is incomplete (only "{{domain}}") and must
mirror the English template; replace the value with the full template including
placeholders for {{domain}}, {{attempt}} and {{error}} — either provide a Nepali
translation of "Certificate for your domain '{{domain}}' could not be generated.
This is attempt no. {{attempt}}, and the failure was caused by: {{error}}" or
temporarily copy the English string exactly so the placeholders are present and
match the en.json source.

"emails.recovery.preview": "{{project}} पासवर्ड रिसेट गर्न यो लिंक प्रयोग गर्नुहोस्।",
"emails.mfaChallenge.signature": "{{project}} टोली",
"emails.mfaChallenge.thanks": "धन्यवाद,",
"emails.mfaChallenge.clientInfo": "यो प्रमाणीकरण कोड {{b}}{{/b}} बाट अनुरोध गरिएको थियो। यदि तपाईंले प्रमाणीकरण कोडको अनुरोध गर्नुभएन भने, तपाईले यो इमेललाई सुरक्षित रूपमा बेवास्ता गर्न सक्नुहुन्छ।",
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Potentially missing context placeholders in mfaChallenge clientInfo

The bold block is empty. This usually includes the client/device/IP details. Please mirror placeholders from the English source to avoid runtime issues.

Examples (to be verified): {{ip}} or {{agentClient}}/{{agentDevice}}/{{agentOs}}.

Run to fetch the reference string and placeholders:


🏁 Script executed:

#!/bin/bash
rg -n -A2 -B2 $'^\\s*"emails\\.mfaChallenge\\.clientInfo"\\s*:' app/config/locale/translations/en.json

Length of output: 681


Add missing agent placeholders to emails.mfaChallenge.clientInfo in ne.json

The English source includes {{agentClient}}, {{agentDevice}}, and {{agentOs}} inside bold tags; the Nepali string currently has empty bold tags — add the same placeholders to avoid runtime interpolation errors.

  • Files:
    • app/config/locale/translations/ne.json — emails.mfaChallenge.clientInfo (line ~13)
    • Reference: app/config/locale/translations/en.json — emails.mfaChallenge.clientInfo

Suggested change:

  • "emails.mfaChallenge.clientInfo": "यो प्रमाणीकरण कोड {{b}}{{/b}} बाट अनुरोध गरिएको थियो। यदि तपाईंले प्रमाणीकरण कोडको अनुरोध गर्नुभएन भने, तपाईले यो इमेललाई सुरक्षित रूपमा बेवास्ता गर्न सक्नुहुन्छ।",
  • "emails.mfaChallenge.clientInfo": "यो प्रमाणीकरण कोड {{b}}{{agentClient}}{{/b}} मा {{b}}{{agentDevice}}{{/b}} {{b}}{{agentOs}}{{/b}} बाट अनुरोध गरिएको थियो। यदि तपाईंले प्रमाणीकरण कोडको अनुरोध गर्नुभएन भने, तपाईले यो इमेललाई सुरक्षित रूपमा बेवास्ता गर्न सक्नुहुन्छ।",
🤖 Prompt for AI Agents
In app/config/locale/translations/ne.json around line 13, the
emails.mfaChallenge.clientInfo string contains empty bold tags which omit the
required placeholders and will cause interpolation errors; update the Nepali
string to include the same placeholders as the English source—add
{{agentClient}}, {{agentDevice}}, and {{agentOs}} wrapped in the bold tags in
the appropriate order and spacing so the translated sentence matches the source
structure and interpolates those values at runtime.

"emails.mfaChallenge.signature": "{{project}} टोली",
"emails.mfaChallenge.thanks": "धन्यवाद,",
"emails.mfaChallenge.clientInfo": "यो प्रमाणीकरण कोड {{b}}{{/b}} बाट अनुरोध गरिएको थियो। यदि तपाईंले प्रमाणीकरण कोडको अनुरोध गर्नुभएन भने, तपाईले यो इमेललाई सुरक्षित रूपमा बेवास्ता गर्न सक्नुहुन्छ।",
"emails.mfaChallenge.description": " तपाईंको दुई चरणको प्रमाणिकरण पुष्टि गर्नको लागि निम्न कोड प्रवेश गर्नुहोस् {{b}}{{/b}}। यो कोड १५ मिनेटमा म्याद सकिनेछ।",
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Missing OTP placeholder in MFA description

The code placeholder inside bold is empty and there’s a leading space.

-    "emails.mfaChallenge.description": " तपाईंको दुई चरणको प्रमाणिकरण पुष्टि गर्नको लागि निम्न कोड प्रवेश गर्नुहोस् {{b}}{{/b}}। यो कोड १५ मिनेटमा म्याद सकिनेछ।",
+    "emails.mfaChallenge.description": "तपाईंको दुई चरण प्रमाणिकरण पुष्टि गर्न निम्न कोड प्रविष्ट गर्नुहोस्: {{b}}{{otp}}{{/b}}। यो कोड १५ मिनेटमा म्याद सकिनेछ।",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"emails.mfaChallenge.description": " तपाईंको दुई चरणको प्रमाणिकरण पुष्टि गर्नको लागि निम्न कोड प्रवेश गर्नुहोस् {{b}}{{/b}}। यो कोड १५ मिनेटमा म्याद सकिनेछ।",
"emails.mfaChallenge.description": "तपाईंको दुई चरण प्रमाणिकरण पुष्टि गर्न निम्न कोड प्रविष्ट गर्नुहोस्: {{b}}{{otp}}{{/b}}। यो कोड १५ मिनेटमा म्याद सकिनेछ।",
🤖 Prompt for AI Agents
In app/config/locale/translations/ne.json around line 14, the MFA description
has an empty bold placeholder and a leading space before the sentence; update
the string to remove the extra leading space and place the OTP placeholder
inside the bold tags (e.g. change "{{b}}{{/b}}" to "{{b}}{{otp}}{{/b}}"),
keeping the surrounding Nepali text intact and preserving punctuation and
spacing.

Comment on lines +52 to +56
$mainJson = \json_decode(\file_get_contents($dir . '/' . $mainFile), true);
$mainKeys = \array_keys($mainJson);

$files = \array_diff(\scandir($dir), array('.', '..', $mainFile));

Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Add JSON read error handling and filter to only .json files

Reading/parsing JSON without checks risks warnings/fatal errors on malformed files, and scanning the dir without filtering may include non-JSON files.

-        $mainJson = \json_decode(\file_get_contents($dir . '/' . $mainFile), true);
-        $mainKeys = \array_keys($mainJson);
-
-        $files = \array_diff(\scandir($dir), array('.', '..', $mainFile));
+        $mainJson = \json_decode(\file_get_contents($dir . '/' . $mainFile), true);
+        if (!\is_array($mainJson)) {
+            Console::error("Failed to parse {$mainFile} as JSON.");
+            return;
+        }
+        $mainKeys = \array_keys($mainJson);
+
+        $files = \array_values(\array_filter(\scandir($dir), function ($f) use ($mainFile) {
+            return $f !== '.' && $f !== '..' && $f !== $mainFile && \str_ends_with($f, '.json');
+        }));

Verification script to detect non-JSON files getting picked up (should return nothing after this change):


🏁 Script executed:

#!/bin/bash
fd . app/config/locale/translations | rg -n --invert-match '\.json$'

Length of output: 70


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Locate file ==="
rg -n "DevGenerateTranslations.php" || true

echo -e "\n=== File content (first 300 lines) ==="
if [ -f "src/Appwrite/Platform/Tasks/DevGenerateTranslations.php" ]; then
  sed -n '1,300p' src/Appwrite/Platform/Tasks/DevGenerateTranslations.php
else
  echo "src/Appwrite/Platform/Tasks/DevGenerateTranslations.php not found"
fi

echo -e "\n=== composer.json (require section) ==="
if [ -f "composer.json" ]; then
  sed -n '1,200p' composer.json
else
  echo "composer.json not found"
fi

Length of output: 10439


Add JSON read/parse checks and filter scanned files to .json only

The file currently reads and parses JSON without checking for read/parse errors and scans the translations directory without filtering by extension. This can produce warnings/fatal errors on unreadable/malformed files or pick up non-JSON files. Apply the changes below.

Files to update:

  • src/Appwrite/Platform/Tasks/DevGenerateTranslations.php

Suggested diff:

-        $mainJson = \json_decode(\file_get_contents($dir . '/' . $mainFile), true);
-        $mainKeys = \array_keys($mainJson);
-
-        $files = \array_diff(\scandir($dir), array('.', '..', $mainFile));
+        $mainPath = $dir . '/' . $mainFile;
+        $mainContent = @\file_get_contents($mainPath);
+        if ($mainContent === false) {
+            Console::error("Failed to read {$mainFile} at {$mainPath}.");
+            return;
+        }
+        $mainJson = \json_decode($mainContent, true);
+        if (!\is_array($mainJson)) {
+            Console::error("Failed to parse {$mainFile} as JSON: " . \json_last_error_msg());
+            return;
+        }
+        $mainKeys = \array_keys($mainJson);
+
+        $files = \array_values(\array_filter(\scandir($dir), function ($f) use ($mainFile, $dir) {
+            return $f !== '.' && $f !== '..' && $f !== $mainFile && \str_ends_with($f, '.json') && \is_file($dir . '/' . $f);
+        }));

Also harden the per-file read/parse and update logic inside the foreach loop — replace the existing unguarded reads with safe checks, for example:

-            $fileJson = \json_decode(\file_get_contents($dir . '/' . $file), true);
-            $fileKeys = \array_keys($fileJson);
+            $filePath = $dir . '/' . $file;
+            $fileContent = @\file_get_contents($filePath);
+            if ($fileContent === false) {
+                Console::warning("Failed to read {$file}; skipping.");
+                continue;
+            }
+            $fileJson = \json_decode($fileContent, true);
+            if (!\is_array($fileJson)) {
+                Console::warning("Skipping {$file}: invalid JSON: " . \json_last_error_msg());
+                continue;
+            }
+            $fileKeys = \array_keys($fileJson);

And inside the non-dry-run update loop, read/decode with checks before each write (or re-use/merge safely and write once), e.g.:

-                        $json = \json_decode(\file_get_contents($dir . '/' . $file), true);
+                        $jsonContent = @\file_get_contents($filePath);
+                        if ($jsonContent === false) {
+                            Console::warning("Failed to read {$file} for update; aborting remaining keys for this file.");
+                            break;
+                        }
+                        $json = \json_decode($jsonContent, true);
+                        if (!\is_array($json)) {
+                            Console::warning("Failed to parse {$file} as JSON during update: " . \json_last_error_msg());
+                            break;
+                        }
...
-                        \file_put_contents($dir . '/' . $file, \json_encode($json, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | 0));
+                        \file_put_contents($filePath, \json_encode($json, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));

These changes ensure:

  • Read failures are detected and logged.
  • JSON parse errors are detected and logged with json_last_error_msg().
  • Only .json files are processed.
  • Corrupt/unreadable files are skipped safely.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$mainJson = \json_decode(\file_get_contents($dir . '/' . $mainFile), true);
$mainKeys = \array_keys($mainJson);
$files = \array_diff(\scandir($dir), array('.', '..', $mainFile));
$mainPath = $dir . '/' . $mainFile;
$mainContent = @\file_get_contents($mainPath);
if ($mainContent === false) {
Console::error("Failed to read {$mainFile} at {$mainPath}.");
return;
}
$mainJson = \json_decode($mainContent, true);
if (!\is_array($mainJson)) {
Console::error("Failed to parse {$mainFile} as JSON: " . \json_last_error_msg());
return;
}
$mainKeys = \array_keys($mainJson);
$files = \array_values(\array_filter(\scandir($dir), function ($f) use ($mainFile, $dir) {
return $f !== '.' && $f !== '..' && $f !== $mainFile && \str_ends_with($f, '.json') && \is_file($dir . '/' . $f);
}));
🤖 Prompt for AI Agents
In src/Appwrite/Platform/Tasks/DevGenerateTranslations.php around lines 52 to
56, the code reads and decodes the main JSON and scans the directory without
error handling or filtering by extension; update it to (1) verify
file_get_contents($dir.'/'.$mainFile) did not return false and handle/log that
error, (2) json_decode the contents and check for errors via json_last_error() /
json_last_error_msg(), (3) replace scandir() result filtering to only include
files ending with .json (and exclude $mainFile), and (4) inside the per-file
loop, guard each file read with file_exists/is_readable, check file_get_contents
and json_decode for errors and skip corrupt/unreadable files while logging, then
merge/update in-memory and perform a single write when not a dry-run (or skip
write in dry-run); ensure every read/parse failure is detected and logged and no
non-.json files are processed.

Comment on lines +64 to +66
$fileJson = \json_decode(\file_get_contents($dir . '/' . $file), true);
$fileKeys = \array_keys($fileJson);

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Guard against invalid locale files

A malformed locale file will cause array_keys() on null. Add a quick guard and continue.

-            $fileJson = \json_decode(\file_get_contents($dir . '/' . $file), true);
-            $fileKeys = \array_keys($fileJson);
+            $fileJson = \json_decode(\file_get_contents($dir . '/' . $file), true);
+            if (!\is_array($fileJson)) {
+                Console::warning("Skipping {$file}: invalid JSON.");
+                continue;
+            }
+            $fileKeys = \array_keys($fileJson);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$fileJson = \json_decode(\file_get_contents($dir . '/' . $file), true);
$fileKeys = \array_keys($fileJson);
$fileJson = \json_decode(\file_get_contents($dir . '/' . $file), true);
if (!\is_array($fileJson)) {
Console::warning("Skipping {$file}: invalid JSON.");
continue;
}
$fileKeys = \array_keys($fileJson);
🤖 Prompt for AI Agents
In src/Appwrite/Platform/Tasks/DevGenerateTranslations.php around lines 64 to
66, json_decode() can return null for malformed locale files which makes
array_keys() throw; after reading and decoding the file, check that the decoded
value is a non-empty array (e.g. is_array($fileJson) && $fileJson !== null) and
if it isn’t, optionally log or warn about the malformed file and continue to the
next file instead of calling array_keys on it.

Comment on lines +83 to +107
$language = \explode('.', $file)[0];

foreach ($missingKeys as $missingKey) {
$json = \json_decode(\file_get_contents($dir . '/' . $file), true);

$translation = $this->generateTranslation($language, $mainJson[$missingKey], $apiKey);

Console::log('Translation results:');
Console::log('English: ' . $mainJson[$missingKey]);
Console::log($language . ': ' . $translation);

$keysProcessed++;

// This puts new key at beginning to prevent merge conflict issue and ending comma
$newPair = [];
$newPair[$missingKey] = $translation;

if (isset($json[$missingKey])) {
unset($json[$missingKey]);
}

$json = \array_merge($newPair, $json);

\file_put_contents($dir . '/' . $file, \json_encode($json, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | 0));
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Avoid N×I/O: Read once, translate all, write once

You re-read and re-write the same file per missing key. That’s slow and fragile. Load once per file, mutate in memory, and write once.

-                    $language = \explode('.', $file)[0];
-
-                    foreach ($missingKeys as $missingKey) {
-                        $json = \json_decode(\file_get_contents($dir . '/' . $file), true);
-
-                        $translation = $this->generateTranslation($language, $mainJson[$missingKey], $apiKey);
-
-                        Console::log('Translation results:');
-                        Console::log('English: ' . $mainJson[$missingKey]);
-                        Console::log($language . ': ' . $translation);
-
-                        $keysProcessed++;
-
-                        // This puts new key at beginning to prevent merge conflict issue and ending comma
-                        $newPair = [];
-                        $newPair[$missingKey] = $translation;
-
-                        if (isset($json[$missingKey])) {
-                            unset($json[$missingKey]);
-                        }
-
-                        $json = \array_merge($newPair, $json);
-
-                        \file_put_contents($dir . '/' . $file, \json_encode($json, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | 0));
-                    }
+                    $language = \explode('.', $file)[0];
+                    $json = \json_decode(\file_get_contents($dir . '/' . $file), true);
+                    foreach ($missingKeys as $missingKey) {
+                        $translation = $this->generateTranslation($language, $mainJson[$missingKey], $apiKey);
+                        Console::log('Translation results:');
+                        Console::log('English: ' . $mainJson[$missingKey]);
+                        Console::log($language . ': ' . $translation);
+                        $keysProcessed++;
+                        // Prepend new pair
+                        $newPair = [$missingKey => $translation];
+                        unset($json[$missingKey]); // ensure no duplicates
+                        $json = \array_merge($newPair, $json);
+                    }
+                    $bytes = \file_put_contents(
+                        $dir . '/' . $file,
+                        \json_encode($json, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
+                    );
+                    if ($bytes === false) {
+                        Console::error("Failed writing {$file}");
+                    }
🤖 Prompt for AI Agents
In src/Appwrite/Platform/Tasks/DevGenerateTranslations.php around lines 83 to
107, the code reads and writes the same file inside the missing-keys loop
causing N×I/O; instead, decode the target file once before the loop, iterate
missingKeys generating translations and mutating the in-memory $json (build new
pairs or prepend them), update $keysProcessed as you go, and after the loop
perform a single json_encode and file_put_contents for that file; also keep the
existing logic that removes any existing key before merging so ordering is
preserved, but move unset/merge operations to operate on the in-memory $json and
perform the write only once per file.

Comment on lines +121 to +130
$pattern = '/{{\w+}}/';

$enTranslation = preg_replace_callback($pattern, function ($match) use (&$id, &$placeholders) {
$placeholders[$id] = $match[0];
$label = \trim($match[0], "{}");
$key = "<m id={$id} label=\"{$label}\" />";
$id++;
return $key;
}, $enTranslation);

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Placeholders regex misses closing tags and printf tokens; can corrupt markup

The regex only matches {{word}}. It won’t protect closing tags like {{/b}} or printf tokens like %s, so the LLM might translate or reorder them, breaking templates. Protect both opening/closing moustaches and common printf tokens.

Apply this diff to harden placeholder protection:

-        $id = 0;
-        $pattern = '/{{\w+}}/';
-
-        $enTranslation = preg_replace_callback($pattern, function ($match) use (&$id, &$placeholders) {
-            $placeholders[$id] = $match[0];
-            $label = \trim($match[0], "{}");
-            $key = "<m id={$id} label=\"{$label}\" />";
-            $id++;
-            return $key;
-        }, $enTranslation);
+        $id = 0;
+        // Protect: {{tag}}, {{/tag}}, {{token.with.dots}}, and common printf tokens (%s, %d, %f, %u)
+        $pattern = '/{{\/?[\w\.]+}}|%[sduf]/';
+
+        $enTranslation = \preg_replace_callback($pattern, function ($match) use (&$id, &$placeholders) {
+            $placeholders[$id] = $match[0];
+            // Label is only for context; keep it simple and safe
+            $label = \str_replace('"', '&quot;', $match[0]);
+            $key = "<m id={$id} label=\"{$label}\" />";
+            $id++;
+            return $key;
+        }, $enTranslation);
@@
-        foreach ($placeholders as $placeholder) {
+        foreach ($placeholders as $placeholder) {
             $pattern = '/\<m id=' . $id . ' label=".+" \/>/';
             $targetTranslation = \preg_replace($pattern, $placeholder, $targetTranslation);
             $id++;
         }

Also applies to: 163-168

🤖 Prompt for AI Agents
In src/Appwrite/Platform/Tasks/DevGenerateTranslations.php around lines 121-130
(and also update the same logic at 163-168): the current placeholder regex only
matches simple {{word}} and will miss closing moustache tags (e.g. {{/b}}) and
printf-style tokens (e.g. %s, %1$s), risking broken markup after translation;
replace the pattern with one that matches opening/closing moustache tags and
common printf tokens (for example an alternation like {{\/?\w+}} or
%(\d+\$)?[sdf] etc.), use that combined pattern in both callback sites, keep
storing the entire matched token into $placeholders[$id] and derive the label by
trimming only the surrounding braces (not removing leading slashes for closing
tags), then increment $id and return the same <m id=... label="..."/> wrapper so
all these tokens are preserved through LLM translation.

Comment on lines +138 to +147
name: 'get_translation',
description: 'Get the translation output from given message in well structured JSON',
object: $object,
required: $object->getNames()
);

$adapter = new OpenAI($apiKey, OpenAI::MODEL_GPT_4O);
$agent = new Agent($adapter);
$agent->setSchema($schema);

Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Validate and harden LLM output handling

You assume the agent returns valid JSON with a “translation” field. If the response is malformed, json_decode returns null and PHP notices follow. Validate and fail-safe.

-        $adapter = new OpenAI($apiKey, OpenAI::MODEL_GPT_4O);
+        $adapter = new OpenAI($apiKey, OpenAI::MODEL_GPT_4O);
         $agent = new Agent($adapter);
         $agent->setSchema($schema);
@@
-        $output = $conversation->send()->getContent();
-
-        $outputJson = \json_decode($output, true);
-
-        $targetTranslation = $outputJson['translation'];
+        $output = $conversation->send()->getContent();
+        $outputJson = \json_decode($output, true);
+        if (!\is_array($outputJson) || !\array_key_exists('translation', $outputJson) || !\is_string($outputJson['translation'])) {
+            Console::warning('LLM returned unexpected payload, falling back to original text.');
+            return $enTranslation;
+        }
+        $targetTranslation = $outputJson['translation'];

Verification: run a quick grep to ensure no other assumptions on response shape exist.

Also applies to: 151-160


🏁 Script executed:

#!/bin/bash
rg -n "getContent\(\).*json_decode" -A2

Length of output: 41


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/Appwrite/Platform/Tasks/DevGenerateTranslations.php"

echo "Checking file presence: $FILE"
if [ -f "$FILE" ]; then
  echo "File found. Showing lines 1-260:"
  nl -ba "$FILE" | sed -n '1,260p'
else
  echo "File not found: $FILE"
fi

echo
echo "Searching repo for getContent( occurrences (with context):"
rg -n --hidden -S "getContent\(" -A3 -B2 || true

echo
echo "Searching repo for json_decode( occurrences (with context):"
rg -n --hidden -S "json_decode\(" -A3 -B2 || true

echo
echo "Searching repo for ->send()->getContent occurrences (with context):"
rg -n --hidden -S "->send\(\)->getContent" -A3 -B2 || true

echo
echo "Searching repo for literal 'translation' usage (with context):"
rg -n --hidden -S "'translation'|\"translation\"" -A3 -B2 || true

echo
echo "Searching repo for OpenAI or Agent instantiations (with context):"
rg -n --hidden -S "OpenAI\(|Agent\(" -A3 -B2 || true

Length of output: 569


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/Appwrite/Platform/Tasks/DevGenerateTranslations.php"

echo "Checking file presence: $FILE"
if [ -f "$FILE" ]; then
  echo "File found. Showing lines 1-260:"
  awk '{printf("%5d: %s\n", NR, $0)}' "$FILE" | sed -n '1,260p'
else
  echo "File not found: $FILE"
  exit 0
fi

echo
echo "Searching repo for getContent( occurrences (with context):"
rg -n --hidden -S "getContent\(" -A3 -B3 || true

echo
echo "Searching repo for json_decode( occurrences (with context):"
rg -n --hidden -S "json_decode\(" -A3 -B3 || true

echo
echo "Searching repo for ->send()->getContent occurrences (with context):"
rg -n --hidden -S "->send\(\)->getContent" -A3 -B3 || true

echo
echo "Searching repo for literal 'translation' usage (with context):"
rg -n --hidden -S "'translation'|\"translation\"" -A3 -B3 || true

echo
echo "Searching repo for OpenAI or Agent instantiations (with context):"
rg -n --hidden -S "OpenAI\(|Agent\(" -A3 -B3 || true

Length of output: 189502


Harden LLM output handling — validate JSON and restore placeholders on fallback

The agent response is parsed without checks; json_decode may return null and accessing ['translation'] will raise notices/errors. I verified the unguarded access in the repo.

  • Locations:
    • src/Appwrite/Platform/Tasks/DevGenerateTranslations.php — lines ~156-160 (unguarded $outputJson['translation'])

Suggested patch:

-        $output = $conversation->send()->getContent();
-
-        $outputJson = \json_decode($output, true);
-
-        $targetTranslation = $outputJson['translation'];
+        $output = $conversation->send()->getContent();
+
+        $outputJson = \json_decode($output, true);
+        if (!\is_array($outputJson) || !\array_key_exists('translation', $outputJson) || !\is_string($outputJson['translation'])) {
+            Console::warning('LLM returned unexpected payload, falling back to original English text.');
+            // Restore placeholders in the original English string before returning
+            $id = 0;
+            $fallback = $enTranslation;
+            foreach ($placeholders as $placeholder) {
+                $pattern = '/\<m id=' . $id . ' label=".+" \/>/';
+                $fallback = \preg_replace($pattern, $placeholder, $fallback);
+                $id++;
+            }
+            return $fallback;
+        }
+        $targetTranslation = $outputJson['translation'];

Please apply the change to ensure safe parsing and a correct fallback that restores placeholders.

Committable suggestion skipped: line range outside the PR's diff.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (5)
src/Appwrite/Platform/Tasks/DevGenerateTranslations.php (5)

54-58: Add JSON read/parse checks and limit to .json files only

Reading/parsing en.json without checks and scanning the dir without filtering risks crashes on malformed files and processing non-JSON files.

-        $mainJson = \json_decode(\file_get_contents($dir . '/' . $mainFile), true);
-        $mainKeys = \array_keys($mainJson);
-
-        $files = \array_diff(\scandir($dir), array('.', '..', $mainFile));
+        $mainPath = $dir . '/' . $mainFile;
+        $mainContent = @\file_get_contents($mainPath);
+        if ($mainContent === false) {
+            Console::error("Failed to read {$mainFile} at {$mainPath}.");
+            return;
+        }
+        $mainJson = \json_decode($mainContent, true);
+        if (!\is_array($mainJson)) {
+            Console::error("Failed to parse {$mainFile} as JSON: " . \json_last_error_msg());
+            return;
+        }
+        $mainKeys = \array_keys($mainJson);
+        // Only process locale JSON files (exclude en.json itself)
+        $files = \array_values(\array_filter(\scandir($dir), function ($f) use ($mainFile, $dir) {
+            return $f !== '.' && $f !== '..' && $f !== $mainFile && \str_ends_with($f, '.json') && \is_file($dir . '/' . $f);
+        }));
+        // Ensure enforced keys exist in English source
+        $enforcedKeys = \array_values(\array_intersect($enforcedKeys, $mainKeys));

62-68: Guard per-locale JSON reads against unreadable/malformed files

array_keys() on null will emit warnings if JSON is invalid; also protect file reads.

-            $fileJson = \json_decode(\file_get_contents($dir . '/' . $file), true);
-            $fileKeys = \array_keys($fileJson);
+            $filePath = $dir . '/' . $file;
+            $fileContent = @\file_get_contents($filePath);
+            if ($fileContent === false) {
+                Console::warning("Failed to read {$file}; skipping.");
+                continue;
+            }
+            $fileJson = \json_decode($fileContent, true);
+            if (!\is_array($fileJson)) {
+                Console::warning("Skipping {$file}: invalid JSON: " . \json_last_error_msg());
+                continue;
+            }
+            $fileKeys = \array_keys($fileJson);

87-111: Avoid N×I/O: load locale once, translate all, write once

Currently the file is re-read and re-written per missing key. Read once, mutate in memory, and write once with error checks.

-                    $language = \explode('.', $file)[0];
-
-                    foreach ($missingKeys as $missingKey) {
-                        $json = \json_decode(\file_get_contents($dir . '/' . $file), true);
-
-                        $translation = $this->generateTranslation($language, $mainJson[$missingKey], $apiKey);
-
-                        Console::log('Translation results:');
-                        Console::log('English: ' . $mainJson[$missingKey]);
-                        Console::log($language . ': ' . $translation);
-
-                        $keysProcessed++;
-
-                        // This puts new key at beginning to prevent merge conflict issue and ending comma
-                        $newPair = [];
-                        $newPair[$missingKey] = $translation;
-
-                        if (isset($json[$missingKey])) {
-                            unset($json[$missingKey]);
-                        }
-
-                        $json = \array_merge($newPair, $json);
-
-                        \file_put_contents($dir . '/' . $file, \json_encode($json, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | 0));
-                    }
+                    $language = \explode('.', $file)[0];
+                    // Work on the already-decoded $fileJson
+                    $json = $fileJson;
+                    $newPairs = [];
+                    foreach ($missingKeys as $missingKey) {
+                        if (!\array_key_exists($missingKey, $mainJson)) {
+                            Console::warning("Skipping {$missingKey}: not found in English source.");
+                            continue;
+                        }
+                        $translation = $this->generateTranslation($language, $mainJson[$missingKey], $apiKey);
+                        Console::log('Translation results:');
+                        Console::log('English: ' . $mainJson[$missingKey]);
+                        Console::log($language . ': ' . $translation);
+                        $keysProcessed++;
+                        if (isset($json[$missingKey])) {
+                            unset($json[$missingKey]); // avoid duplicates
+                        }
+                        $newPairs[$missingKey] = $translation; // collect to prepend later
+                    }
+                    if (!empty($newPairs)) {
+                        // Prepend all new pairs at once
+                        $json = \array_merge($newPairs, $json);
+                        $bytes = \file_put_contents(
+                            $filePath,
+                            \json_encode($json, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
+                        );
+                        if ($bytes === false) {
+                            Console::error("Failed writing {$file}");
+                        }
+                    }

124-133: Harden placeholder protection: support closing tags and printf tokens

The current regex only matches {{word}}. Protect {{/tag}}, dotted tokens, and printf-like placeholders to avoid corrupting templates.

-        $id = 0;
-        $pattern = '/{{\w+}}/';
-
-        $enTranslation = preg_replace_callback($pattern, function ($match) use (&$id, &$placeholders) {
-            $placeholders[$id] = $match[0];
-            $label = \trim($match[0], "{}");
-            $key = "<m id={$id} label=\"{$label}\" />";
-            $id++;
-            return $key;
-        }, $enTranslation);
+        $id = 0;
+        // Protect: {{tag}}, {{/tag}}, {{token.with.dots}}, and common printf tokens (%s, %d, %f, %u, and positional)
+        $pattern = '/{{\/?[\w\.]+}}|%(\d+\$)?[sduf]/';
+
+        $enTranslation = \preg_replace_callback($pattern, function ($match) use (&$id, &$placeholders) {
+            $placeholders[$id] = $match[0];
+            // Keep label simple and safe
+            $label = \str_replace('"', '&quot;', $match[0]);
+            $key = "<m id={$id} label=\"{$label}\" />";
+            $id++;
+            return $key;
+        }, $enTranslation);

160-165: Validate LLM output and provide safe fallback restoring placeholders

Assuming a well-formed JSON with a 'translation' field is brittle. Add guards and fallback to the original text with placeholders restored.

-        $output = $conversation->send()->getContent();
-
-        $outputJson = \json_decode($output, true);
-
-        $targetTranslation = $outputJson['translation'];
+        $output = $conversation->send()->getContent();
+        $outputJson = \json_decode($output, true);
+        if (!\is_array($outputJson) || !\array_key_exists('translation', $outputJson) || !\is_string($outputJson['translation'])) {
+            Console::warning('LLM returned unexpected payload, falling back to original text.');
+            // Restore placeholders in the original English string before returning
+            $id = 0;
+            $fallback = $enTranslation;
+            foreach ($placeholders as $placeholder) {
+                $pattern = '/\<m id=' . $id . ' label=".+" \/>/';
+                $fallback = \preg_replace($pattern, $placeholder, $fallback);
+                $id++;
+            }
+            return $fallback;
+        }
+        $targetTranslation = $outputJson['translation'];
🧹 Nitpick comments (2)
src/Appwrite/Platform/Tasks/DevGenerateTranslations.php (2)

110-110: Nit: remove superfluous bitwise OR flag

| 0 is a no-op in json_encode options.

-                        \file_put_contents($dir . '/' . $file, \json_encode($json, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | 0));
+                        \file_put_contents($dir . '/' . $file, \json_encode($json, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));

78-86: Optional: batch logging and rate-limit to avoid noisy output

When many keys are missing, per-key logs can flood output and slow runs. Consider batching summaries or a --verbose toggle.

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these settings in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between f79d57e and 6aa0ac3.

📒 Files selected for processing (1)
  • src/Appwrite/Platform/Tasks/DevGenerateTranslations.php (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/Appwrite/Platform/Tasks/DevGenerateTranslations.php (2)
src/Appwrite/Platform/Services/Tasks.php (2)
  • Tasks (24-49)
  • __construct (26-48)
.github/workflows/static-analysis/locale/index.js (2)
  • files (27-29)
  • missingKeys (80-80)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: E2E Service Test (Sites)

Comment on lines +41 to +43
$dryRun = \strval($dryRun) === 'true';
$enforcedKeys = \explode(',', $enforcedKeys);

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Critical: dry-run parsing bug flips true to false

strval(true) === 'true' is false (strval(true) yields "1"), so dry-run can unintentionally be disabled. Also, normalize and trim enforced keys upfront.

-        $dryRun = \strval($dryRun) === 'true';
-        $enforcedKeys = \explode(',', $enforcedKeys);
+        // Robust boolean parsing for CLI input
+        $dryRun = \is_bool($dryRun) ? $dryRun : \filter_var((string)$dryRun, FILTER_VALIDATE_BOOLEAN);
+        // Normalize enforced keys: split, trim, drop empties
+        $enforcedKeys = \array_values(\array_filter(\array_map('trim', \explode(',', $enforcedKeys)), 'strlen'));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$dryRun = \strval($dryRun) === 'true';
$enforcedKeys = \explode(',', $enforcedKeys);
// Robust boolean parsing for CLI input
$dryRun = \is_bool($dryRun) ? $dryRun : \filter_var((string)$dryRun, FILTER_VALIDATE_BOOLEAN);
// Normalize enforced keys: split, trim, drop empties
$enforcedKeys = \array_values(\array_filter(\array_map('trim', \explode(',', $enforcedKeys)), 'strlen'));
🤖 Prompt for AI Agents
In src/Appwrite/Platform/Tasks/DevGenerateTranslations.php around lines 41 to
43, the dry-run parsing using strval($dryRun) === 'true' is incorrect
(strval(true) == "1") and will flip true to false; replace it with a robust
boolean normalization such as using filter_var($dryRun, FILTER_VALIDATE_BOOLEAN)
or an explicit check for the string 'true' (case-insensitive) so true values are
preserved; also normalize enforcedKeys by exploding, trimming each key, and
removing empty entries (e.g., array_map('trim', ... ) + array_filter) so you
don't pass blank keys forward.

Comment on lines +69 to +76
$missingKeys = [];
foreach ($mainKeys as $key) {
if (!(\in_array($key, $fileKeys)) && !\str_starts_with($key, 'mock')) {
$missingKeys[] = $key;
} elseif (\in_array($key, $enforcedKeys)) {
$missingKeys[] = $key;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Compute missing keys using sets; enforce keys explicitly and avoid O(n²)

Use hash sets for membership, include enforced keys regardless of presence, and avoid duplicates.

-            $missingKeys = [];
-            foreach ($mainKeys as $key) {
-                if (!(\in_array($key, $fileKeys)) && !\str_starts_with($key, 'mock')) {
-                    $missingKeys[] = $key;
-                } elseif (\in_array($key, $enforcedKeys)) {
-                    $missingKeys[] = $key;
-                }
-            }
+            $missingKeys = [];
+            $fileKeysSet = \array_flip($fileKeys);
+            $enforcedSet = \array_flip($enforcedKeys);
+            foreach ($mainKeys as $key) {
+                if (isset($enforcedSet[$key])) { // regenerate even if present
+                    $missingKeys[] = $key;
+                    continue;
+                }
+                if (!isset($fileKeysSet[$key]) && !\str_starts_with($key, 'mock')) {
+                    $missingKeys[] = $key;
+                }
+            }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$missingKeys = [];
foreach ($mainKeys as $key) {
if (!(\in_array($key, $fileKeys)) && !\str_starts_with($key, 'mock')) {
$missingKeys[] = $key;
} elseif (\in_array($key, $enforcedKeys)) {
$missingKeys[] = $key;
}
}
$missingKeys = [];
$fileKeysSet = \array_flip($fileKeys);
$enforcedSet = \array_flip($enforcedKeys);
foreach ($mainKeys as $key) {
if (isset($enforcedSet[$key])) { // regenerate even if present
$missingKeys[] = $key;
continue;
}
if (!isset($fileKeysSet[$key]) && !\str_starts_with($key, 'mock')) {
$missingKeys[] = $key;
}
}
🤖 Prompt for AI Agents
In src/Appwrite/Platform/Tasks/DevGenerateTranslations.php around lines 69 to
76, the current loop does O(n²) membership checks and can add duplicates;
replace the logic to use hash-set style membership (e.g. flip or associative
map) for $fileKeys and $enforcedKeys to achieve O(1) lookups, iterate $mainKeys
once and add a key to a result associative map only if it is not in $fileKeys
and does not start with 'mock', then after the loop ensure all $enforcedKeys are
present in the result map (add them unconditionally), and finally convert the
result map back to a plain indexed array (or use array_keys) to avoid
duplicates.

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