Repository navigation
fix(i18n): consolidate the open community translation PRs - #12514
Conversation
…1889) SidebarToolbar is a React.memo boundary whose props are shallow-equal on a language switch, so without its own useTranslation() subscription it kept the English copy it rendered at boot — the persisted locale is applied asynchronously, after the lazy catalog loads. Co-authored-by: 5Hyeons <ohs2251@naver.com>
formatTrackingSince() used the OS locale rather than the language chosen in Settings. getIntlLocale() resolves the active i18n language to a BCP-47 tag, mapping the synthetic plugin<hex> resource language that Intl rejects back to the pack's real locale. Co-authored-by: Evgenii <kumiro@me.com>
protectedTranslation refused every language-pack key under auto.components.settings.plugin*, which caught 104 keys that carry no trust meaning — section titles, empty states, Refresh, Add path. A 35-path exact allowlist opens those while consent, provenance, and every *Failed string stay protected; anything new stays protected until it is added deliberately. PluginsSettingsSection.experimental is held back from the contributed allowlist: the "Experimental" chip is a trust badge, which the module's own boundary comment places out of scope. Co-authored-by: Evgenii <kumiro@me.com>
Every card under Settings -> Integrations built its status pill from bare literals inside the JSX call, so the pill stayed English beside a translated description. audit-localization-coverage.mjs inspects JSX attributes and object properties, not conditional expressions handed to a prop, so the gate stayed green while the strings shipped untranslated. The three token-configured cards share tokenProviderStatusLabel(); Gitea passes optional: true because it works read-only without a token, which is why its unconfigured state reads Optional setup rather than Not configured. Co-authored-by: Evgenii <kumiro@me.com>
…9444) The description was assembled from a bare template literal, so it stayed English under every language pack. It now lives in the catalog as two entries — description with a {{shortcut}} placeholder and descriptionBase for the invert-on variant — following the copy-module pattern from #10991. Co-authored-by: 조재중 <126754298+m-a-king@users.noreply.github.com>
The shared Automation tour copy was rendered without passing through translate(), and the overlay surface hardcoded its default Next and Done labels. Copy is keyed off the step id rather than its position, so inserting a step ahead of them cannot shift the text onto the wrong step. Co-authored-by: 5Hyeons <ohs2251@naver.com>
SidebarNav.c86d83b5c3 is the onboarding pill rendered beside Orca Mobile, so
"New" marks a new feature. Both locales had translated it as a create action —
ko 새로 만들기 ("create new"), zh 新建 ("create new") — which reads as a button.
The PR's other hunk (zh GH PR) already landed on main and resolved to a no-op.
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
The badge value is pinned in locale-key-overrides.mjs, so the ko/zh fix from #10664 would have been reverted by the next catalog repair. ja carried the same defect — 新規 reads as "create new" — and is corrected alongside.
185 zh values were still verbatim English, plus the 6 VoiceMicrophoneSetting keys were missing. Placeholders, "X of Y" counts, and key ordering are unchanged; the only edits to already-translated values are punctuation. Applied at key level rather than as a branch merge — the PR was cut from an older base and conflicted only on JSON context, with no value drift against main. Co-authored-by: 闲人 <38777313+qiuyongjin@users.noreply.github.com> Co-authored-by: jake <qiu5630@163.com>
The browser.loadFailure.* keys were still raw English in es/ja/ko/zh. en.json is
untouched; every {{value0}} token and the Orca/HTTPS brand terms are preserved.
13 of the zh keys were already covered by #12368, so only the 6 it did not
reach are taken here.
Co-authored-by: MumuTW <42820974+MumuTW@users.noreply.github.com>
The source-control primary action rendered 押す ("press") and 引く ("pull a
physical object") for Push/Pull. プッシュ/プル match the sibling フォースプッシュ
and 同期 labels. A guard test pins both so bootstrap re-translation cannot
silently regress them.
Co-authored-by: Iris-Fla <103801589+Iris-Fla@users.noreply.github.com>
Nine ko values said something other than the English source. The riskiest is
SourceControl.6d7f2a47e5 "Discard folder", rendered as 폴더 삭제 ("delete
folder") next to a sibling delete-untracked action. Others: "Only branches Orca
named itself" read as "branches named Orca"; "staged changes" as 단계적
("phased"); "first-party cloud" as the mojibake 1方클라우드; "discard the
deletion" as "the deletion is deleted"; "Stage all changes" as a sentence
meaning "prepare"; and Recipes as 조리법 (cooking recipes).
EphemeralVmsPane.skillTitle is dropped from the PR's test and override — the
key was renamed to cloudVmSkillTitle on main, so the assertion would resolve to
undefined.
Co-authored-by: ShinSungkyu <kxu4583@naver.com>
…c terms to English (#12192) Fixes #12113. shouldPreserveEnglishValue keyed on the English value, so any key whose source string equalled a NEVER_TRANSLATE_VALUES entry was forced back to English on every repair run — agent, commit, repo, terminal and Continue were all on that list. Measured on a clean checkout: ko 279, ja 461, zh 1180, es 479 values rewritten, the large majority destroying translator work. 17 generic terms move into locale-generic-ui-terms.mjs, and the brand revert now skips a term's canonical rendering, so genuinely nonsensical forms (zh 回购, ja/zh 端子, es Comprometerse) still fire while 터미널/커밋/エージェント survive. Brand, path, and code tokens are untouched — MD -> 医学博士 and HEAD -> CABEZA are why that list still earns its keep. No catalog values change; every file is under config/scripts/. Co-authored-by: AnddyAgudelo <44873492+AnddyAgudelo@users.noreply.github.com>
The override for SshTargetForm.137e88ce8d held a truncated relay-TTL sentence ending mid-clause at "최대:", which renders "Timeout after disconnect (seconds)" — the sibling key 55c56cf2c7 — not its own English source. #12192 only corrected the terminals token inside that wrong sentence. Point it at the value ko.json already ships, so a catalog repair cannot overwrite the correct string with the wrong one.
…#11728) en.json carries ~190 keys per locale that the locale catalogs have not been bootstrapped with yet, so every repair-locale-catalog run threw a TypeError before doing any work. Skip missing leaves instead. Split out of #11728 so the crash fix can land without the catalog regeneration, which still needs native-speaker review. Co-authored-by: Turtle-Hwan <turtlehwan@gmail.com> Co-authored-by: Orca <help@stably.ai>
Eight renderer call sites built Intl.RelativeTimeFormat(undefined, ...), which resolves to the OS locale, so relative timestamps rendered in Korean on a Korean-locale machine even with the UI language set to English. Unlike DateTimeFormat, RelativeTimeFormat emits language words, so it must follow the UI language. Rebased onto main: the formatter now resolves through getIntlLocale() (#12105) rather than i18n.resolvedLanguage, so the synthetic plugin<hex> resource language cannot reach the constructor and throw. GitHubItemDialog and PullRequestPage no longer own their formatter — main moved it into work-item-state-presentation, which is converted instead, along with the newly added site there. Co-authored-by: moseoh <azqazq195@gmail.com> Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Co-authored-by: Orca <help@stably.ai>
…5640) Most of #5640 landed independently, but 12 strings were still falling through to English: the ko "Diverged" pull-policy notice, and the ja source-control primary-action blocked reasons. Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Co-authored-by: Orca <help@stably.ai>
…8816) Cherry-picked the unambiguous subset of #8816: - 디렉토리 -> 디렉터리 and 쉘 -> 셸, the standard loanword transcriptions - Milestones: 이정표 (a signpost) -> 마일스톤, the Linear product noun - Permission granted: 허가 (a licence) -> 권한 - "privacy envelope" rendered as 봉투, a paper envelope - commentTooLarge: 너무 커서 (too bulky) -> 너무 길어, across 9 composers The rest of the PR is left out: 83 leaves are agent -> Agent recapitalizations that fight the catalog convention, 17 are reverted by the repair policy, and several change meaning (viewed -> 읽음 "read" on the GitHub file checkbox, which means seen; "resolve PR base" read as fixing a problem; 차이점 -> diff, which also breaks ko search recall). Co-authored-by: tykimseoul <tykimseoul@gmail.com> Co-authored-by: Orca <help@stably.ai>
The build-time repair layer carried overrides that rewrite already-correct CJK values back to English. Most are inert against today's catalogs but fire on the next regeneration, so they read as latent regressions rather than policy: - zh workspace status picker (Play/Flag/Zinc/Rose/Emerald/Amber/Violet/Sky/ Blue/Neutral) and `sheet`/`page` were pinned to English while every sibling option, and ko/ja/es, stay translated — half a Chinese picker. - The zh `蓝色的`/`琥珀色`/`中性的` phrase fixes correctly flagged the adjectival 的 form but replaced it with English instead of the bare color noun. - ja `Play` was pinned to English though the catalog already reads 再生. - A value-wide zh `Open: '进行中'` mapped every "Open" to "in progress", including the button that opens an MCP config file. "Open" is a verb (打开) on buttons and a state (开放) beside 已关闭, so no single mapping fits. Catalog corrections in the same area: - The GitHub/PR state picker key override read 진행 중 / 进行中 for ko and zh while ja already had the correct オープン; now 열림 / 开放, matching 닫힘 / 已关闭 on the sibling entry. - The terminal cursor-color group is the on-screen cursor, not the Cursor editor; ja already had カーソル, ko/zh now get 커서/光标 instead of "Cursor". - Tailwind swatch labels 天空 (the sky) and 锌 (the metal) do not read as colors; now 天蓝/锌灰, and ja 空 becomes 空色. - The ko disk-usage heading was pinned to bare "Space" while its own description says 저장 공간; both now use 저장 공간. Two policy tests pinned the Play de-localization. They diagnosed the input correctly — 玩 / 遊ぶ are wrong for a play icon — so the expectations move to 播放 / 再生 rather than English. Destructive drift (localized -> English on regeneration) drops from 40 to 21 for zh, 6 to 5 for ja, and 5 to 3 for ko. What remains is deliberate: search qualifiers, path and filename literals, and product names.
📝 WalkthroughWalkthroughThe changes revise locale repair rules so generic workflow terms remain translated while brands remain in English. They add catalog regression checks and update localized catalog overrides. Renderer code now uses shared locale-aware relative-time formatting and valid Intl locale resolution. Contextual tours and browser routing descriptions use localized copy. Integration status labels are centralized and translated. Plugin language packs can translate an explicit UI-chrome allowlist while protected security copy remains restricted. 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/renderer/src/i18n/locales/en.json (1)
13499-13524: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winFix casing on the new "next" tour-button string.
Line 13503 adds
"ffa4412b66": "next"in lowercase. The sibling strings in the same object —"Back","Skip tour","Dismiss tour", and the newly added"complete": "Done"— all use capitalized button-label casing.Line 13514 adds
"38b3155418": "Next"(capitalized) in the closely relatedcontextual.tour.overlay.measurementnamespace within this same diff, confirming the expected casing for this concept.Capitalize
"next"to match the sibling labels and the sibling "Next" string added in this PR.✏️ Proposed fix
- "ffa4412b66": "next", + "ffa4412b66": "Next",
🧹 Nitpick comments (1)
src/shared/plugins/plugin-language-pack-artifact.test.ts (1)
60-72: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winAdd coverage for the nested
plugins.searchcontainer.The allowlist in
src/shared/plugins/plugin-translatable-chrome.tsincludesauto.components.settings.plugins.search.*(Lines 59-65). This fixture createscomponentas one object key, so it cannot exercise both container checks insrc/shared/plugins/plugin-language-pack-artifact.ts(Lines 101-106). A dotted component value would hit the existing dotted-key rejection in Lines 147-154. Add a nested fixture and assertok: trueand the translated value incatalog.Suggested test
+ it('accepts nested settings-search chrome', () => { + const result = parsePluginLanguagePackArtifact( + JSON.stringify({ + auto: { + components: { + settings: { + plugins: { search: { title: 'Поиск' } } + } + } + } + }) + ) + expect(result).toMatchObject({ + ok: true, + catalog: { + auto: { + components: { + settings: { plugins: { search: { title: 'Поиск' } } } + } + } + } + }) + })
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 2ff7452c-2d2f-4cae-b5c5-e39c4dfb39f6
📒 Files selected for processing (63)
config/scripts/locale-cross-locale-key-overrides.mjsconfig/scripts/locale-generic-ui-terms.mjsconfig/scripts/locale-generic-ui-terms.test.mjsconfig/scripts/locale-ja-value-overrides.mjsconfig/scripts/locale-key-overrides.mjsconfig/scripts/locale-ko-key-overrides.jsonconfig/scripts/locale-ko-value-overrides.mjsconfig/scripts/locale-phrase-fixes.mjsconfig/scripts/locale-prose-term-exemptions.mjsconfig/scripts/locale-repair-catalog-missing-leaves.test.mjsconfig/scripts/locale-translation-policy-ko-round5.test.mjsconfig/scripts/locale-translation-policy.es-round5.test.mjsconfig/scripts/locale-translation-policy.ja-round5.test.mjsconfig/scripts/locale-translation-policy.mjsconfig/scripts/locale-translation-policy.test.mjsconfig/scripts/locale-translation-policy.zh-round5.test.mjsconfig/scripts/locale-value-overrides.mjsconfig/scripts/locale-zh-value-overrides.mjsconfig/scripts/verify-localization-catalog.mjsconfig/scripts/verify-localization-catalog.test.mjssrc/renderer/src/components/JiraIssueWorkspace.tsxsrc/renderer/src/components/LinearItemDrawer.tsxsrc/renderer/src/components/TaskPage.tsxsrc/renderer/src/components/activity/ActivityPrototypePage.tsxsrc/renderer/src/components/contextual-tours/ContextualTourOverlaySurface.localization.test.tsxsrc/renderer/src/components/contextual-tours/ContextualTourOverlaySurface.tsxsrc/renderer/src/components/contextual-tours/contextual-tour-overlay-measurement.test.tssrc/renderer/src/components/contextual-tours/contextual-tour-overlay-measurement.tssrc/renderer/src/components/github/work-item-state-presentation.tsxsrc/renderer/src/components/linear-issue-workspace-text.tssrc/renderer/src/components/settings/AccountsPane.tsxsrc/renderer/src/components/settings/BrowserLinkRoutingModifierSetting.tsxsrc/renderer/src/components/settings/BrowserPane.tsxsrc/renderer/src/components/settings/browser-link-routing-copy.tssrc/renderer/src/components/settings/browser-link-routing-localization.test.tssrc/renderer/src/components/settings/browser-search.test.tssrc/renderer/src/components/settings/browser-search.tssrc/renderer/src/components/settings/jira-integration-card.tsxsrc/renderer/src/components/settings/task-tracker-integration-cards.tsxsrc/renderer/src/components/settings/token-source-control-integration-cards.tsxsrc/renderer/src/components/settings/token-source-control-status.tssrc/renderer/src/components/sidebar/SidebarToolbar.test.tsxsrc/renderer/src/components/sidebar/SidebarToolbar.tsxsrc/renderer/src/components/stats/StatsPane.tsxsrc/renderer/src/components/status-bar/workspace-space-format.test.tssrc/renderer/src/components/status-bar/workspace-space-format.tssrc/renderer/src/i18n/i18n.tssrc/renderer/src/i18n/integration-card-status-localization.test.tssrc/renderer/src/i18n/intl-locale.test.tssrc/renderer/src/i18n/ja-technical-literal-mistranslations.test.tssrc/renderer/src/i18n/ko-ui-semantic-mistranslations.test.tssrc/renderer/src/i18n/locales/en.jsonsrc/renderer/src/i18n/locales/es.jsonsrc/renderer/src/i18n/locales/ja.jsonsrc/renderer/src/i18n/locales/ko.jsonsrc/renderer/src/i18n/locales/zh.jsonsrc/renderer/src/i18n/plugin-chrome-allowlist.test.tssrc/renderer/src/i18n/relative-time-format.test.tssrc/renderer/src/i18n/relative-time-format.tssrc/shared/contextual-tours.tssrc/shared/plugins/plugin-language-pack-artifact.test.tssrc/shared/plugins/plugin-language-pack-artifact.tssrc/shared/plugins/plugin-translatable-chrome.ts
💤 Files with no reviewable changes (1)
- config/scripts/locale-prose-term-exemptions.mjs
| "legacyHelp": "Update this server manually once to enable remote updates." | ||
| }, | ||
| "BrowserLinkRoutingSetting": { | ||
| "description": "Abre los enlaces http(s) del terminal, Markdown y el editor en el navegador integrado de Orca. {{shortcut}} siempre usa el navegador del sistema.", |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Clarify the modifier action.
Line 9683 omits that the user must hold {{shortcut}}. Use wording such as Al mantener pulsado {{shortcut}}, se usa siempre el navegador del sistema.
| "f0c6e2a581": "This branch is not ready for a {{value0}} yet.", | ||
| "b8e4f2a901": "リモート操作が終了するまでお待ちください。", | ||
| "c9f3a1b802": "{{value0}} を作成する前に競合を解決してください。", | ||
| "d2a8c4e703": "このブランチに {{value0}} に含める変更はありません。", |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Fix the Japanese grammar in the no-changes message.
Line 11030 uses two に particles in a way that makes the sentence unclear. Keep {{value0}} unchanged and use a structure such as the following.
Proposed fix
- "d2a8c4e703": "このブランチに {{value0}} に含める変更はありません。",
+ "d2a8c4e703": "このブランチには、{{value0}} に含める変更がありません。",📝 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.
| "d2a8c4e703": "このブランチに {{value0}} に含める変更はありません。", | |
| "d2a8c4e703": "このブランチには、{{value0}} に含める変更がありません。", |
| "f4c0e6a925": "{{value0}} を作成する前に変更を commit してください。", | ||
| "a5d1f7b036": "{{value0}} を作成する前に commit を公開してください。", | ||
| "b6e2a8c147": "{{value0}} を作成する前に commit をプッシュしてください。", |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Use consistent Japanese spelling for commit.
Lines 11032-11034 use Latin commit, but Line 11039 uses コミット. Use コミット in these messages to keep the source-control terminology consistent.
Proposed fix
- "f4c0e6a925": "{{value0}} を作成する前に変更を commit してください。",
- "a5d1f7b036": "{{value0}} を作成する前に commit を公開してください。",
- "b6e2a8c147": "{{value0}} を作成する前に commit をプッシュしてください。",
+ "f4c0e6a925": "{{value0}} を作成する前に変更をコミットしてください。",
+ "a5d1f7b036": "{{value0}} を作成する前にコミットを公開してください。",
+ "b6e2a8c147": "{{value0}} を作成する前にコミットをプッシュしてください。",📝 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.
| "f4c0e6a925": "{{value0}} を作成する前に変更を commit してください。", | |
| "a5d1f7b036": "{{value0}} を作成する前に commit を公開してください。", | |
| "b6e2a8c147": "{{value0}} を作成する前に commit をプッシュしてください。", | |
| "f4c0e6a925": "{{value0}} を作成する前に変更をコミットしてください。", | |
| "a5d1f7b036": "{{value0}} を作成する前にコミットを公開してください。", | |
| "b6e2a8c147": "{{value0}} を作成する前にコミットをプッシュしてください。", |
| export function formatUiRelativeTimeFromDate(input: string, fallback = 'recently'): string { | ||
| const date = new Date(input) | ||
| if (Number.isNaN(date.getTime())) { | ||
| return fallback |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Localize the invalid-date fallback.
formatUiRelativeTimeFromDate() returns literal English "recently" for invalid input. All current callers use this default. Non-English UI languages will show English text when a provider returns an invalid timestamp.
src/renderer/src/i18n/relative-time-format.ts#L33-L36: Resolve the default fallback through a translation key, or require callers to provide a localized fallback.src/renderer/src/i18n/relative-time-format.test.ts#L55-L58: Add a non-English assertion for the localized invalid-date fallback.
📍 Affects 2 files
src/renderer/src/i18n/relative-time-format.ts#L33-L36(this comment)src/renderer/src/i18n/relative-time-format.test.ts#L55-L58
|
Thanks for consolidating these, and for the rebase-merge that kept per-commit authorship intact. One thing worth correcting in the PR body: the Credit column doesn't match the actual PR authors. Git authorship on
The rows for #11728, #8816 and #8662 are right as written. If it's useful next time, the column can be derived rather than typed: |
|
Following up on my note above with one detail that changes what the fix should be — the Credit column can't be corrected row by row, because several of the handles don't resolve at all. Checked each one against
So four of the credited handles are not GitHub accounts, and two belong to real people with no PRs here — The three rows that are right are the three whose handles resolve to actual contributors, which suggests the column came from somewhere other than the PR metadata rather than being shuffled. Regenerating it beats patching rows: for pr in 11889 12105 12455 12208 9444 12270 10664 12368 10672 12301 11169 12192 11728 8662 5640 8816; do
printf '#%s @%s\n' "$pr" "$(gh api repos/stablyai/orca/pulls/$pr --jq .user.login)"
doneGit authorship on |
Consolidates the open community translation PRs and issues into one reviewable branch. Every commit keeps its original author or co-author trailer.
What landed
repairCatalogcrashing on un-bootstrapped locale keysTwo commits have no upstream PR: a stale
SshTargetFormko override found while merging, and the de-localization sweep described below.Notes on the non-trivial merges
#11728 reported a
TypeErrorin the localization build. It reproduces onmain:en.jsoncarries 188 ko / 193 ja / 183 zh / 193 es keys that the locale catalogs were never bootstrapped with, andapplyCjkLatinTermSpacingcalls.replaceon the resultingundefined. Only the four-line guard and a regression test are here; the catalog regeneration that PR also carried is left for a separate change.#8662 fixed relative times rendering in the OS locale rather than the UI language.
mainhad since extractedformatRelativeTimeintowork-item-state-presentation.tsx, so this is a reimplementation rather than a merge. Routing throughgetIntlLocale()also closes aRangeErrorthat plugin language packs would otherwise hit, since they register under a synthetic tagIntlrejects.De-localization sweep. While reconciling the above I found repair-policy overrides that rewrite already-correct CJK values back to English — a value-wide zh
Open: '进行中'that also hit the button opening an MCP config file, ten workspace-status swatch labels pinned to English beside translated siblings, and a terminal cursor-color group treated as the Cursor editor. Most are inert against today's catalogs and fire on the next regeneration. Destructive drift drops from 40 to 21 for zh, 6 to 5 for ja, and 5 to 3 for ko; what remains is deliberate (search qualifiers, path literals, product names).Not included
New-locale PRs (#7974, #6526, #10934 pt-BR · #11577 tr · #10604, #7801 zh-TW · #7111 es-419 · #8923) and issues #9207, #10592 stay open. They are good work; the plan is to support them through plugin language packs rather than to close them.
#9294 is punted.
Verification
verify:localization-catalog,verify:localization-extraction,verify:localization-coverage, andtypecheckall pass. Full suite: 45,468 passing, 2 failures insrc/relay/agent-exec-handler.test.tsthat reproduce identically on a cleanorigin/main(ambientGIT_CONFIG_COUNTleaking into the spawned environment).Made with Orca 🐋