mirror of
https://we.phorge.it/source/phorge.git
synced 2025-01-01 18:30:59 +01:00
Update the translations document
Summary: Fixes T8616. The rules for contributors have come up a few times recently, so update this document to give more complete advice. Also try to do a better job with "adding new classes" (previously: libphutil libraries blah blah no one cares). Test Plan: Read documents. Reviewers: btrahan, joshuaspence Reviewed By: btrahan, joshuaspence Subscribers: joshuaspence, epriestley Maniphest Tasks: T8616 Differential Revision: https://secure.phabricator.com/D13358
This commit is contained in:
parent
9921cbc41a
commit
85083c88c1
10 changed files with 609 additions and 195 deletions
|
@ -2,7 +2,7 @@
|
||||||
"name": "phabcontrib",
|
"name": "phabcontrib",
|
||||||
"title": "Phabricator Contributor Documentation",
|
"title": "Phabricator Contributor Documentation",
|
||||||
"short": "Phabricator Contributor Docs",
|
"short": "Phabricator Contributor Docs",
|
||||||
"preface": "Information for Phabricator contributors.",
|
"preface": "Information for Phabricator contributors and developers.",
|
||||||
"root": "../../../",
|
"root": "../../../",
|
||||||
"uri.source":
|
"uri.source":
|
||||||
"https://secure.phabricator.com/diffusion/P/browse/master/%f$%l",
|
"https://secure.phabricator.com/diffusion/P/browse/master/%f$%l",
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"name": "phabdev",
|
"name": "phabdev",
|
||||||
"title": "Phabricator Technical Documentation",
|
"title": "Phabricator Technical Documentation",
|
||||||
"short": "Phabricator Tech Docs",
|
"short": "Phabricator Tech Docs",
|
||||||
"preface": "Technical documentation intended for Phabricator developers.",
|
"preface": "Technical reference material for Phabricator developers.",
|
||||||
"root": "../../../",
|
"root": "../../../",
|
||||||
"uri.source":
|
"uri.source":
|
||||||
"https://secure.phabricator.com/diffusion/P/browse/master/%f$%l",
|
"https://secure.phabricator.com/diffusion/P/browse/master/%f$%l",
|
||||||
|
|
256
src/docs/contributor/adding_new_classes.diviner
Normal file
256
src/docs/contributor/adding_new_classes.diviner
Normal file
|
@ -0,0 +1,256 @@
|
||||||
|
@title Adding New Classes
|
||||||
|
@group developer
|
||||||
|
|
||||||
|
Guide to adding new classes to extend Phabricator.
|
||||||
|
|
||||||
|
Overview
|
||||||
|
========
|
||||||
|
|
||||||
|
Phabricator is highly modular, and many parts of it can be extended by adding
|
||||||
|
new classes. This document explains how to write new classes to change or
|
||||||
|
expand the behavior of Phabricator.
|
||||||
|
|
||||||
|
IMPORTANT: The upstream does not offer support with extension development.
|
||||||
|
|
||||||
|
Fundamentals
|
||||||
|
============
|
||||||
|
|
||||||
|
Phabricator primarily discovers functionality by looking at concrete subclasses
|
||||||
|
of some base class. For example, Phabricator determines which applications are
|
||||||
|
available by looking at all of the subclasses of
|
||||||
|
@{class@phabricator:PhabricatorApplication}. It
|
||||||
|
discovers available workflows in `arc` by looking at all of the subclasses of
|
||||||
|
@{class@arcanist:ArcanistWorkflow}. It discovers available locales
|
||||||
|
by looking at all of the subclasses of @{class@libphutil:PhutilLocale}.
|
||||||
|
|
||||||
|
This pattern holds in many cases, so you can often add functionality by adding
|
||||||
|
new classes with no other work. Phabricator will automatically discover and
|
||||||
|
integrate the new capabilities or features at runtime.
|
||||||
|
|
||||||
|
There are two main ways to add classes:
|
||||||
|
|
||||||
|
- **Extensions Directory**: This is a simple way to add new code. It is
|
||||||
|
less powerful, but takes a lot less work. This is good for quick changes,
|
||||||
|
testing and development, or getting started on a larger project.
|
||||||
|
- **Creating Libraries**: This is a more advanced and powerful way to
|
||||||
|
organize extension code. This is better for larger or longer-lived
|
||||||
|
projects, or any code which you plan to distribute.
|
||||||
|
|
||||||
|
The next sections walk through these approaches in greater detail.
|
||||||
|
|
||||||
|
|
||||||
|
Extensions Directory
|
||||||
|
====================
|
||||||
|
|
||||||
|
The easiest way to extend Phabricator by adding new classes is to drop them
|
||||||
|
into the extensions directory, at `phabricator/src/extensions/`.
|
||||||
|
|
||||||
|
This is intended as a quick way to add small pieces of functionality, test new
|
||||||
|
features, or get started on a larger project. Extending Phabricator like this
|
||||||
|
imposes a small performance penalty compared to using a library.
|
||||||
|
|
||||||
|
This directory exists in all libphutil libraries, so you can find similar
|
||||||
|
directories in `arcanist/src/extensions/` and `libphutil/src/extensions/`.
|
||||||
|
|
||||||
|
For example, to add a new application, create a file like this one and add it
|
||||||
|
to `phabricator/src/extensions/`.
|
||||||
|
|
||||||
|
```name=phabricator/src/extensions/ExampleApplication.php, lang=php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class ExampleApplication extends PhabricatorApplication {
|
||||||
|
|
||||||
|
public function getName() {
|
||||||
|
return pht('Example');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If you load {nav Applications} in the web UI, you should now see your new
|
||||||
|
application in the list. It won't do anything yet since you haven't defined
|
||||||
|
any interesting behavior, but this is the basic building block of Phabricator
|
||||||
|
extensions.
|
||||||
|
|
||||||
|
|
||||||
|
Creating Libraries
|
||||||
|
==================
|
||||||
|
|
||||||
|
A more powerful (but more complicated) way to extend Phabricator is to create
|
||||||
|
a libphutil library. Libraries can organize a larger amount of code, are easier
|
||||||
|
to work with and distribute, and have slightly better performance than loose
|
||||||
|
source files in the extensions directory.
|
||||||
|
|
||||||
|
In general, you'll perform these one-time setup steps to create a library:
|
||||||
|
|
||||||
|
- Create a new directory.
|
||||||
|
- Use `arc liberate` to initialize and name the library.
|
||||||
|
- Configure Phabricator or Arcanist to load the library.
|
||||||
|
|
||||||
|
Then, to add new code, you do this:
|
||||||
|
|
||||||
|
- Write or update classes.
|
||||||
|
- Update the library metadata by running `arc liberate` again.
|
||||||
|
|
||||||
|
Initializing a Library
|
||||||
|
======================
|
||||||
|
|
||||||
|
To create a new libphutil library, create a directory for it and run
|
||||||
|
`arc liberate` on the directory. This documentation will use a conventional
|
||||||
|
directory layout, which is recommended, but you are free to deviate from this.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ mkdir libcustom/
|
||||||
|
$ cd libcustom/
|
||||||
|
libcustom/ $ arc liberate src/
|
||||||
|
```
|
||||||
|
|
||||||
|
Now you'll get a prompt like this:
|
||||||
|
|
||||||
|
```lang=txt
|
||||||
|
No library currently exists at that path...
|
||||||
|
The directory '/some/path/libcustom/src' does not exist.
|
||||||
|
|
||||||
|
Do you want to create it? [y/N] y
|
||||||
|
Creating new libphutil library in '/some/path/libcustom/src'.
|
||||||
|
Choose a name for the new library.
|
||||||
|
|
||||||
|
What do you want to name this library?
|
||||||
|
```
|
||||||
|
|
||||||
|
Choose a library name (in this case, "libcustom" would be appropriate) and it
|
||||||
|
you should get some details about the library initialization:
|
||||||
|
|
||||||
|
```lang=txt
|
||||||
|
Writing '__phutil_library_init__.php' to
|
||||||
|
'/some/path/libcustom/src/__phutil_library_init__.php'...
|
||||||
|
Using library root at 'src'...
|
||||||
|
Mapping library...
|
||||||
|
Verifying library...
|
||||||
|
Finalizing library map...
|
||||||
|
OKAY Library updated.
|
||||||
|
```
|
||||||
|
|
||||||
|
This will write three files:
|
||||||
|
|
||||||
|
- `src/.phutil_module_cache` This is a cache which makes "arc liberate"
|
||||||
|
faster when you run it to update the library. You can safely remove it at
|
||||||
|
any time. If you check your library into version control, you can add this
|
||||||
|
file to ignore rules (like `.gitignore`).
|
||||||
|
- `src/__phutil_library_init__.php` This records the name of the library and
|
||||||
|
tells libphutil that a library exists here.
|
||||||
|
- `src/__phutil_library_map__.php` This is a map of all the symbols
|
||||||
|
(functions and classes) in the library, which allows them to be autoloaded
|
||||||
|
at runtime and dependencies to be statically managed by `arc liberate`.
|
||||||
|
|
||||||
|
Linking with Phabricator
|
||||||
|
========================
|
||||||
|
|
||||||
|
If you aren't using this library with Phabricator (e.g., you are only using it
|
||||||
|
with Arcanist or are building something else on libphutil) you can skip this
|
||||||
|
step.
|
||||||
|
|
||||||
|
But, if you intend to use this library with Phabricator, you need to define its
|
||||||
|
dependency on Phabricator by creating a `.arcconfig` file which points at
|
||||||
|
Phabricator. For example, you might write this file to
|
||||||
|
`libcustom/.arcconfig`:
|
||||||
|
|
||||||
|
```lang=json
|
||||||
|
{
|
||||||
|
"load": [
|
||||||
|
"phabricator/src/"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
For details on creating a `.arcconfig`, see
|
||||||
|
@{article:Arcanist User Guide: Configuring a New Project}. In general, this
|
||||||
|
tells `arc liberate` that it should look for symbols in Phabricator when
|
||||||
|
performing static analysis.
|
||||||
|
|
||||||
|
NOTE: If Phabricator isn't located next to your custom library, specify a
|
||||||
|
path which actually points to the `phabricator/` directory.
|
||||||
|
|
||||||
|
You do not need to declare dependencies on `arcanist` or `libphutil`,
|
||||||
|
since `arc liberate` automatically loads them.
|
||||||
|
|
||||||
|
Finally, edit your Phabricator config to tell it to load your library at
|
||||||
|
runtime, by adding it to `load-libraries`:
|
||||||
|
|
||||||
|
```lang=json
|
||||||
|
...
|
||||||
|
'load-libraries' => array(
|
||||||
|
'libcustom' => 'libcustom/src/',
|
||||||
|
),
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
Now, Phabricator will be able to load classes from your custom library.
|
||||||
|
|
||||||
|
|
||||||
|
Writing Classes
|
||||||
|
===============
|
||||||
|
|
||||||
|
To actually write classes, create a new module and put code in it:
|
||||||
|
|
||||||
|
libcustom/ $ mkdir src/example/
|
||||||
|
libcustom/ $ nano src/example/ExampleClass.php # Edit some code.
|
||||||
|
|
||||||
|
Now, run `arc liberate` to regenerate the static resource map:
|
||||||
|
|
||||||
|
libcustom/ $ arc liberate src/
|
||||||
|
|
||||||
|
This will automatically regenerate the static map of the library.
|
||||||
|
|
||||||
|
|
||||||
|
What You Can Extend And Invoke
|
||||||
|
==============================
|
||||||
|
|
||||||
|
libphutil, Arcanist and Phabricator are strict about extensibility of classes
|
||||||
|
and visibility of methods and properties. Most classes are marked `final`, and
|
||||||
|
methods have the minimum required visibility (protected or private). The goal
|
||||||
|
of this strictness is to make it clear what you can safely extend, access, and
|
||||||
|
invoke, so your code will keep working as the upstream changes.
|
||||||
|
|
||||||
|
IMPORTANT: We'll still break APIs frequently. The upstream does not support
|
||||||
|
extension development, and none of these APIs are stable.
|
||||||
|
|
||||||
|
When developing libraries to work with libphutil, Arcanist and Phabricator, you
|
||||||
|
should respect method and property visibility.
|
||||||
|
|
||||||
|
If you want to add features but can't figure out how to do it without changing
|
||||||
|
Phabricator code, here are some approaches you may be able to take:
|
||||||
|
|
||||||
|
- {icon check, color=green} **Use Composition**: If possible, use composition
|
||||||
|
rather than extension to build your feature.
|
||||||
|
- {icon check, color=green} **Find Another Approach**: Check the
|
||||||
|
documentation for a better way to accomplish what you're trying to do.
|
||||||
|
- {icon check, color=green} **File a Feature Request**: Let us know what your
|
||||||
|
use case is so we can make the class tree more flexible or configurable, or
|
||||||
|
point you at the right way to do whatever you're trying to do, or explain
|
||||||
|
why we don't let you do it. Note that we **do not support** extension
|
||||||
|
development so you may have mixed luck with this one.
|
||||||
|
|
||||||
|
These approaches are **discouraged**, but also possible:
|
||||||
|
|
||||||
|
- {icon times, color=red} **Fork**: Create an ad-hoc local fork and remove
|
||||||
|
`final` in your copy of the code. This will make it more difficult for you
|
||||||
|
to upgrade in the future, although it may be the only real way forward
|
||||||
|
depending on what you're trying to do.
|
||||||
|
- {icon times, color=red} **Use Reflection**: You can use
|
||||||
|
[[ http://php.net/manual/en/book.reflection.php | Reflection ]] to remove
|
||||||
|
modifiers at runtime. This is fragile and discouraged, but technically
|
||||||
|
possible.
|
||||||
|
- {icon times, color=red} **Remove Modifiers**: Send us a patch removing
|
||||||
|
`final` (or turning `protected` or `private` into `public`). We will almost
|
||||||
|
never accept these patches unless there's a very good reason that the
|
||||||
|
current behavior is wrong.
|
||||||
|
|
||||||
|
|
||||||
|
Next Steps
|
||||||
|
==========
|
||||||
|
|
||||||
|
Continue by:
|
||||||
|
|
||||||
|
- visiting the [[ https://secure.phabricator.com/w/community_resources/ |
|
||||||
|
Community Resources ]] page to find or share extensions and libraries.
|
|
@ -9,57 +9,370 @@ Overview
|
||||||
Phabricator partially supports internationalization, but many of the tools
|
Phabricator partially supports internationalization, but many of the tools
|
||||||
are missing or in a prototype state.
|
are missing or in a prototype state.
|
||||||
|
|
||||||
This document very briefly summarizes some of what exists today.
|
This document describes what tools exist today, how to add new translations,
|
||||||
|
and how to use the translation tools to make a codebase translatable.
|
||||||
|
|
||||||
|
|
||||||
|
Adding a New Locale
|
||||||
|
===================
|
||||||
|
|
||||||
|
To add a new locale, subclass @{class:PhutilLocale}. This allows you to
|
||||||
|
introduce a new locale, like "German" or "Klingon".
|
||||||
|
|
||||||
|
Once you've created a locale, applications can add translations for that
|
||||||
|
locale.
|
||||||
|
|
||||||
|
For instructions on adding new classes, see @{article:Adding New Classes}.
|
||||||
|
|
||||||
|
|
||||||
|
Adding Translations to Locale
|
||||||
|
=============================
|
||||||
|
|
||||||
|
To translate strings, subclass @{class:PhutilTranslation}. Translations need
|
||||||
|
to belong to a locale: the locale defines an available language, and each
|
||||||
|
translation subclass provides strings for it.
|
||||||
|
|
||||||
|
Translations are separated from locales so that third-party applications can
|
||||||
|
provide translations into different locales without needing to define those
|
||||||
|
locales themselves.
|
||||||
|
|
||||||
|
For instructions on adding new classes, see @{article:Adding New Classes}.
|
||||||
|
|
||||||
|
|
||||||
Writing Translatable Code
|
Writing Translatable Code
|
||||||
========
|
=========================
|
||||||
|
|
||||||
Strings are marked for translation with @{function@libphutil:pht}.
|
Strings are marked for translation with @{function@libphutil:pht}.
|
||||||
|
|
||||||
Adding a New Locale
|
The `pht()` function takes a string (and possibly some parameters) and returns
|
||||||
=========
|
the translated version of that string in the current viewer's locale, if a
|
||||||
|
translation is available.
|
||||||
|
|
||||||
To add a new locale, subclass @{class:PhutilLocale}.
|
If text strings will ultimately be read by humans, they should essentially
|
||||||
|
always be wrapped in `pht()`. For example:
|
||||||
|
|
||||||
Translating Strings
|
```lang=php
|
||||||
========
|
$dialog->appendParagraph(pht('This is an example.'));
|
||||||
|
```
|
||||||
|
|
||||||
|
This allows the code to return the correct Spanish or German or Russian
|
||||||
|
version of the text, if the viewer is using Phabricator in one of those
|
||||||
|
languages and a translation is available.
|
||||||
|
|
||||||
|
Using `pht()` properly so that strings are translatable can be tricky. Briefly,
|
||||||
|
the major rules are:
|
||||||
|
|
||||||
|
- Only pass static strings as the first parameter to `pht()`.
|
||||||
|
- Use parameters to create strings containing user names, object names, etc.
|
||||||
|
- Translate full sentences, not sentence fragments.
|
||||||
|
- Let the translation framework handle plural rules.
|
||||||
|
- Use @{class@libphutil:PhutilNumber} for numbers.
|
||||||
|
- Let the translation framework handle subject gender rules.
|
||||||
|
- Translate all human-readable text, even exceptions and error messages.
|
||||||
|
|
||||||
|
See the next few sections for details on these rules.
|
||||||
|
|
||||||
|
|
||||||
|
Use Static Strings
|
||||||
|
==================
|
||||||
|
|
||||||
|
The first parameter to `pht()` must always be a static string. Broadly, this
|
||||||
|
means it should not contain variables or function or method calls (it's OK to
|
||||||
|
split it across multiple lines and concatenate the parts together).
|
||||||
|
|
||||||
|
These are good:
|
||||||
|
|
||||||
|
```lang=php
|
||||||
|
pht('The night is dark.');
|
||||||
|
pht(
|
||||||
|
'Two roads diverged in a yellow wood, '.
|
||||||
|
'and sorry I could not travel both '.
|
||||||
|
'and be one traveler, long I stood.');
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
These won't work (they might appear to work, but are wrong):
|
||||||
|
|
||||||
|
```lang=php, counterexample
|
||||||
|
pht(some_function());
|
||||||
|
pht('The duck says, '.$quack);
|
||||||
|
pht($string);
|
||||||
|
```
|
||||||
|
|
||||||
|
The first argument must be a static string so it can be extracted by static
|
||||||
|
analysis tools and dumped in a big file for translators. If it contains
|
||||||
|
functions or variables, it can't be extracted, so translators won't be able to
|
||||||
|
translate it.
|
||||||
|
|
||||||
|
Lint will warn you about problems with use of static strings in calls to
|
||||||
|
`pht()`.
|
||||||
|
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
==========
|
||||||
|
|
||||||
|
You can provide parameters to a translation string by using `sprintf()`-style
|
||||||
|
patterns in the input string. For example:
|
||||||
|
|
||||||
|
```lang=php
|
||||||
|
pht('%s earned an award.', $actor);
|
||||||
|
pht('%s closed %s.', $actor, $task);
|
||||||
|
```
|
||||||
|
|
||||||
|
This is primarily appropriate for usernames, object names, counts, and
|
||||||
|
untranslatable strings like URIs or instructions to run commands from the CLI.
|
||||||
|
|
||||||
|
Parameters normally should not be used to combine two pieces of translated
|
||||||
|
text: see the next section for guidance.
|
||||||
|
|
||||||
|
Sentence Fragments
|
||||||
|
==================
|
||||||
|
|
||||||
|
You should almost always pass the largest block of text to `pht()` that you
|
||||||
|
can. Particularly, it's important to pass complete sentences, not try to build
|
||||||
|
a translation by stringing together sentence fragments.
|
||||||
|
|
||||||
|
There are several reasons for this:
|
||||||
|
|
||||||
|
- It gives translators more context, so they can be more confident they are
|
||||||
|
producing a satisfying, natural-sounding translation which will make sense
|
||||||
|
and sound good to native speakers.
|
||||||
|
- In some languages, one fragment may need to translate differently depending
|
||||||
|
on what the other fragment says.
|
||||||
|
- In some languages, the most natural-sounding translation may change the
|
||||||
|
order of words in the sentence.
|
||||||
|
|
||||||
|
For example, suppose we want to translate these sentence to give the user some
|
||||||
|
instructions about how to use an interface:
|
||||||
|
|
||||||
|
> Turn the switch to the right.
|
||||||
|
|
||||||
|
> Turn the switch to the left.
|
||||||
|
|
||||||
|
> Turn the dial to the right.
|
||||||
|
|
||||||
|
> Turn the dial to the left.
|
||||||
|
|
||||||
|
Maybe we have a function like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
function get_string($is_switch, $is_right) {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
One way to write the function body would be like this:
|
||||||
|
|
||||||
|
```lang=php, counterexample
|
||||||
|
$what = $is_switch ? pht('switch') : pht('dial');
|
||||||
|
$dir = $is_right ? pht('right') : pht('left');
|
||||||
|
|
||||||
|
return pht('Turn the ').$what.pht(' to the ').$dir.pht('.');
|
||||||
|
```
|
||||||
|
|
||||||
|
This will work fine in English, but won't work well in other languages.
|
||||||
|
|
||||||
|
One problem with doing this is handling gendered nouns. Languages like Spanish
|
||||||
|
have gendered nouns, where some nouns are "masculine" and others are
|
||||||
|
"feminine". The gender of a noun affects which article (in English, the word
|
||||||
|
"the" is an article) should be used with it.
|
||||||
|
|
||||||
|
In English, we say "**the** knob" and "**the** switch", but a Spanish speaker
|
||||||
|
would say "**la** perilla" and "**el** interruptor", because the noun for
|
||||||
|
"knob" in Spanish is feminine (so it is used with the article "la") while the
|
||||||
|
noun for "switch" is masculine (so it is used with the article "el").
|
||||||
|
|
||||||
|
A Spanish speaker can not translate the string "Turn the" correctly without
|
||||||
|
knowing which gender the noun has. Spanish has //two// translations for this
|
||||||
|
string ("Gira el", "Gira la"), and the form depends on which noun is being
|
||||||
|
used.
|
||||||
|
|
||||||
|
Another problem is that this reduces flexibility. Translating fragments like
|
||||||
|
this locks translators into a specific word order, when rearranging the words
|
||||||
|
might make the sentence sound much more natural to a native speaker.
|
||||||
|
|
||||||
|
For example, if the string read "The knob, to the right, turn it.", it
|
||||||
|
would technically be English and most English readers would understand the
|
||||||
|
meaning, but no native English speaker would speak or write like this.
|
||||||
|
|
||||||
|
However, some languages have different subject-verb order rules or
|
||||||
|
colloquisalisms, and a word order which transliterates like this may sound more
|
||||||
|
natural to a native speaker. By translating fragments instead of complete
|
||||||
|
sentences, you lock translators into English word order.
|
||||||
|
|
||||||
|
Finally, the last fragment is just a period. If a translator is presented with
|
||||||
|
this string in an interface without much context, they have no hope of guessing
|
||||||
|
how it is used in the software (it could be an end-of-sentence marker, or a
|
||||||
|
decimal point, or a date separator, or a currency separator, all of which have
|
||||||
|
very different translations in many locales). It will also conflict with all
|
||||||
|
other translations of the same string in the codebase, so even if they are
|
||||||
|
given context they can't translate it without technical problems.
|
||||||
|
|
||||||
|
To avoid these issues, provide complete sentences for translation. This almost
|
||||||
|
always takes the form of writing out alternatives in full. This is a good way
|
||||||
|
to implement the example function:
|
||||||
|
|
||||||
|
```lang=php
|
||||||
|
if ($is_switch) {
|
||||||
|
if ($is_right) {
|
||||||
|
return pht('Turn the switch to the right.');
|
||||||
|
} else {
|
||||||
|
return pht('Turn the switch to the left.');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if ($is_right) {
|
||||||
|
return pht('Turn the dial to the right.');
|
||||||
|
} else {
|
||||||
|
return pht('Turn the dial to the left.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Although this is more verbose, translators can now get genders correct,
|
||||||
|
rearrange word order, and have far more context when translating. This enables
|
||||||
|
better, natural-sounding translations which are more satisfying to native
|
||||||
|
speakers.
|
||||||
|
|
||||||
To translate strings, subclass @{class:PhutilTranslation}.
|
|
||||||
|
|
||||||
Singular and Plural
|
Singular and Plural
|
||||||
========
|
===================
|
||||||
|
|
||||||
Different languages have various rules for using singular and plural. All you
|
Different languages have various rules for plural nouns.
|
||||||
need to do is to call @{function@libphutil:pht} with a text that is suitable for
|
|
||||||
both forms. Example:
|
|
||||||
|
|
||||||
pht('%d beer(s)', $count);
|
In English there are usually two plural noun forms: for one thing, and any
|
||||||
|
other number of things. For example, we say that one chair is a "chair" and any
|
||||||
|
other number of chairs are "chairs": "0 chairs", "1 chair", "2 chairs", etc.
|
||||||
|
|
||||||
Translators will translate this text for all different forms the language uses:
|
In other languages, there are different (and, in some cases, more) plural
|
||||||
|
forms. For example, in Czech, there are separate forms for "one", "several",
|
||||||
|
and "many".
|
||||||
|
|
||||||
// English translation
|
Because plural noun rules depend on the language, you should not write code
|
||||||
array('%d beer', '%d beers');
|
which hard-codes English rules. For example, this won't translate well:
|
||||||
|
|
||||||
// Czech translation
|
```lang=php, counterexample
|
||||||
array('%d pivo', '%d piva', '%d piv');
|
if ($count == 1) {
|
||||||
|
return pht('This will take an hour.');
|
||||||
|
} else {
|
||||||
|
return pht('This will take hours.');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
The ugly identifier passed to @{function@libphutil:pht} will remain in the text
|
This code is hard-coding the English rule for plural nouns. In languages like
|
||||||
only if the translation doesn't exist.
|
Czech, the correct word for "hours" may be different if the count is 2 or 15,
|
||||||
|
but a translator won't be able to provide the correct translation if the string
|
||||||
|
is written like this.
|
||||||
|
|
||||||
|
Instead, pass a generic string to the translation engine which //includes// the
|
||||||
|
number of objects, and let it handle plural nouns. This is the correct way to
|
||||||
|
write the translation:
|
||||||
|
|
||||||
|
```lang=php
|
||||||
|
return pht('This will take %s hour(s).', new PhutilNumber($count));
|
||||||
|
```
|
||||||
|
|
||||||
|
If you now load the web UI, you'll see "hour(s)" literally in the UI. To fix
|
||||||
|
this so the translation sounds better in English, provide translations for this
|
||||||
|
string in the @{class@phabricator:PhabricatorUSEnglishTranslation} file:
|
||||||
|
|
||||||
|
```lang=php
|
||||||
|
'This will take %s hour(s).' => array(
|
||||||
|
'This will take an hour.',
|
||||||
|
'This will take hours.',
|
||||||
|
),
|
||||||
|
```
|
||||||
|
|
||||||
|
The string will then sound natural in English, but non-English translators will
|
||||||
|
also be able to produce a natural translation.
|
||||||
|
|
||||||
|
Note that the translations don't actually include the number in this case. The
|
||||||
|
number is being passed from the code, but that just lets the translation engine
|
||||||
|
get the rules right: the number does not need to appear in the final
|
||||||
|
translations shown to the user.
|
||||||
|
|
||||||
|
Using PhutilNumber
|
||||||
|
==================
|
||||||
|
|
||||||
|
When translating numbers, you should almost always use `%s` and wrap the count
|
||||||
|
or number in `new PhutilNumber($count)`. For example:
|
||||||
|
|
||||||
|
```lang=php
|
||||||
|
pht('You have %s experience point(s).', new PhutilNumber($xp));
|
||||||
|
```
|
||||||
|
|
||||||
|
This will let the translation engine handle plural noun rules correctly, and
|
||||||
|
also format large numbers correctly in a locale-aware way with proper unit and
|
||||||
|
decimal separators (for example, `1000000` may be printed as "1,000,000",
|
||||||
|
with commas for readability).
|
||||||
|
|
||||||
|
The exception to this rule is IDs which should not be written with unit
|
||||||
|
separators. For example, this is correct for an object ID:
|
||||||
|
|
||||||
|
```lang=php
|
||||||
|
pht('This diff has ID %d.', $diff->getID());
|
||||||
|
```
|
||||||
|
|
||||||
Male and Female
|
Male and Female
|
||||||
========
|
===============
|
||||||
|
|
||||||
Different languages use different words for talking about males, females and
|
Different languages also use different words for talking about subjects who are
|
||||||
unknown genders. Callsites have to call @{function@libphutil:pht} passing
|
male, female or have an unknown gender. In English this is mostly just
|
||||||
@{class:PhabricatorUser} (or other implementation of
|
pronouns (like "he" and "she") but there are more complex rules in other
|
||||||
@{interface@libphutil:PhutilPerson}) if talking about the user. Example:
|
languages, and languages like Czech also require verb agreement.
|
||||||
|
|
||||||
pht('%s wrote', $actor);
|
When a parameter refers to a gendered person, pass an object which implements
|
||||||
|
@{interface@libphutil:PhutilPerson} to `pht()` so translators can provide
|
||||||
|
gendered translation variants.
|
||||||
|
|
||||||
Translators will create this translations:
|
```lang=php
|
||||||
|
pht('%s wrote', $actor);
|
||||||
|
```
|
||||||
|
|
||||||
// English translation
|
Translators will create these translations:
|
||||||
'%s wrote';
|
|
||||||
|
|
||||||
// Czech translation
|
```lang=php
|
||||||
array('%s napsal', '%s napsala');
|
// English translation
|
||||||
|
'%s wrote';
|
||||||
|
|
||||||
|
// Czech translation
|
||||||
|
array('%s napsal', '%s napsala');
|
||||||
|
```
|
||||||
|
|
||||||
|
(You usually don't need to worry very much about this rule, it is difficult to
|
||||||
|
get wrong in standard code.)
|
||||||
|
|
||||||
|
|
||||||
|
Exceptions and Errors
|
||||||
|
=====================
|
||||||
|
|
||||||
|
You should translate all human-readable text, even exceptions and error
|
||||||
|
messages. This is primarily a rule of convenience which is straightforward
|
||||||
|
and easy to follow, not a technical rule.
|
||||||
|
|
||||||
|
Some exceptions and error messages don't //technically// need to be translated,
|
||||||
|
as they will never be shown to a user, but many exceptions and error messages
|
||||||
|
are (or will become) user-facing on some way. When writing a message, there is
|
||||||
|
often no clear and objective way to determine which type of message you are
|
||||||
|
writing. Rather than try to distinguish which are which, we simply translate
|
||||||
|
all human-readable text. This rule is unambiguous and easy to follow.
|
||||||
|
|
||||||
|
In cases where similar error or exception text is often repeated, it is
|
||||||
|
probably appropriate to define an exception for that category of error rather
|
||||||
|
than write the text out repeatedly, anyway. Two examples are
|
||||||
|
@{class@libphutil:PhutilInvalidStateException} and
|
||||||
|
@{class@libphutil:PhutilMethodNotImplementedException}, which mostly exist to
|
||||||
|
produce a consistent message about a common error state in a convenient way.
|
||||||
|
|
||||||
|
There are a handful of error strings in the codebase which may be used before
|
||||||
|
the translation framework is loaded, or may be used during handling other
|
||||||
|
errors, possibly rasised from within the translation framework. This handful
|
||||||
|
of special cases are left untranslated to prevent fatals and cycles in the
|
||||||
|
error handler.
|
||||||
|
|
||||||
|
|
||||||
|
Next Steps
|
||||||
|
==========
|
||||||
|
|
||||||
|
Continue by:
|
||||||
|
|
||||||
|
- adding a new locale or translation file with @{article:Adding New Classes}.
|
||||||
|
|
|
@ -207,5 +207,5 @@ integrations, see the base class for your application and
|
||||||
Continue by:
|
Continue by:
|
||||||
|
|
||||||
- learning more about extending Phabricator with custom code in
|
- learning more about extending Phabricator with custom code in
|
||||||
@{article:libphutil Libraries User Guide};
|
@{article@contributor:Adding New Classes};
|
||||||
- or returning to the @{article: Configuration Guide}.
|
- or returning to the @{article: Configuration Guide}.
|
||||||
|
|
|
@ -109,7 +109,7 @@ This daemon will daemonize and run normally.
|
||||||
just those started with `phd start`. If you're writing a restart script,
|
just those started with `phd start`. If you're writing a restart script,
|
||||||
have it launch any custom daemons explicitly after `phd restart`.
|
have it launch any custom daemons explicitly after `phd restart`.
|
||||||
- You can write your own daemons and manage them with `phd` by extending
|
- You can write your own daemons and manage them with `phd` by extending
|
||||||
@{class:PhabricatorDaemon}. See @{article:libphutil Libraries User Guide}.
|
@{class:PhabricatorDaemon}. See {article@contributor:Adding New Classes}.
|
||||||
- See @{article:Diffusion User Guide} for details about tuning the repository
|
- See @{article:Diffusion User Guide} for details about tuning the repository
|
||||||
daemon.
|
daemon.
|
||||||
|
|
||||||
|
@ -137,4 +137,4 @@ Continue by:
|
||||||
|
|
||||||
- learning about the repository daemon with @{article:Diffusion User Guide};
|
- learning about the repository daemon with @{article:Diffusion User Guide};
|
||||||
or
|
or
|
||||||
- writing your own daemons with @{article:libphutil Libraries User Guide}.
|
- writing your own daemons with {article@contributor:Adding New Classes}.
|
||||||
|
|
|
@ -38,7 +38,7 @@ make this work, you need to do three things:
|
||||||
|
|
||||||
If you haven't created a library for the class to live in yet, you need to do
|
If you haven't created a library for the class to live in yet, you need to do
|
||||||
that first. Follow the instructions in
|
that first. Follow the instructions in
|
||||||
@{article:libphutil Libraries User Guide}, then make the library loadable by
|
@{article@contributor:Adding New Classes}, then make the library loadable by
|
||||||
adding it to your `.arcconfig` like this:
|
adding it to your `.arcconfig` like this:
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|
|
@ -47,7 +47,7 @@ Other options include:
|
||||||
|
|
||||||
- **load**: list of additional Phutil libraries to load at startup.
|
- **load**: list of additional Phutil libraries to load at startup.
|
||||||
See below for details about path resolution, or see
|
See below for details about path resolution, or see
|
||||||
@{article:libphutil Libraries User Guide} for a general introduction to
|
@{article@contributor:Adding New Classes} for a general introduction to
|
||||||
libphutil libraries.
|
libphutil libraries.
|
||||||
- **https.cabundle**: specifies the path to an alternate certificate bundle
|
- **https.cabundle**: specifies the path to an alternate certificate bundle
|
||||||
for use when making HTTPS connections.
|
for use when making HTTPS connections.
|
||||||
|
|
|
@ -21,7 +21,7 @@ To install event listeners in Phabricator, follow these steps:
|
||||||
|
|
||||||
- Write a listener class which extends @{class@libphutil:PhutilEventListener}.
|
- Write a listener class which extends @{class@libphutil:PhutilEventListener}.
|
||||||
- Add it to a libphutil library, or create a new library (for instructions,
|
- Add it to a libphutil library, or create a new library (for instructions,
|
||||||
see @{article:libphutil Libraries User Guide}.
|
see @{article@contributor:Adding New Classes}.
|
||||||
- Configure Phabricator to load the library by adding it to `load-libraries`
|
- Configure Phabricator to load the library by adding it to `load-libraries`
|
||||||
in the Phabricator config.
|
in the Phabricator config.
|
||||||
- Configure Phabricator to install the event listener by adding the class
|
- Configure Phabricator to install the event listener by adding the class
|
||||||
|
@ -38,7 +38,7 @@ To install event listeners in Arcanist, follow these steps:
|
||||||
|
|
||||||
- Write a listener class which extends @{class@libphutil:PhutilEventListener}.
|
- Write a listener class which extends @{class@libphutil:PhutilEventListener}.
|
||||||
- Add it to a libphutil library, or create a new library (for instructions,
|
- Add it to a libphutil library, or create a new library (for instructions,
|
||||||
see @{article:libphutil Libraries User Guide}.
|
see @{article@contributor:Adding New Classes}.
|
||||||
- Configure Phabricator to load the library by adding it to `load`
|
- Configure Phabricator to load the library by adding it to `load`
|
||||||
in the Arcanist config (e.g., `.arcconfig`, or user/global config).
|
in the Arcanist config (e.g., `.arcconfig`, or user/global config).
|
||||||
- Configure Arcanist to install the event listener by adding the class
|
- Configure Arcanist to install the event listener by adding the class
|
||||||
|
|
|
@ -1,155 +0,0 @@
|
||||||
@title libphutil Libraries User Guide
|
|
||||||
@group userguide
|
|
||||||
|
|
||||||
Guide to creating and managing libphutil libraries.
|
|
||||||
|
|
||||||
= Overview =
|
|
||||||
|
|
||||||
libphutil includes a library system which organizes PHP classes and functions
|
|
||||||
into modules. Some extensions and customizations of Arcanist and Phabricator
|
|
||||||
require you to make code available to Phabricator by providing it in a libphutil
|
|
||||||
library.
|
|
||||||
|
|
||||||
For example, if you want to store files in some kind of custom storage engine,
|
|
||||||
you need to write a class which can interact with that engine and then tell
|
|
||||||
Phabricator to load it.
|
|
||||||
|
|
||||||
In general, you perform these one-time setup steps:
|
|
||||||
|
|
||||||
- Create a new directory.
|
|
||||||
- Use `arc liberate` to initialize and name the library.
|
|
||||||
- Add a dependency on Phabricator if necessary.
|
|
||||||
- Add the library to your Phabricator config or `.arcconfig` so it will be
|
|
||||||
loaded at runtime.
|
|
||||||
|
|
||||||
Then, to add new code, you do this:
|
|
||||||
|
|
||||||
- Write or update classes.
|
|
||||||
- Update the library metadata by running `arc liberate` again.
|
|
||||||
|
|
||||||
= Creating a New Library =
|
|
||||||
|
|
||||||
To **create a new libphutil library**:
|
|
||||||
|
|
||||||
$ mkdir libcustom/
|
|
||||||
$ cd libcustom/
|
|
||||||
libcustom/ $ arc liberate src/
|
|
||||||
|
|
||||||
Now you'll get a prompt like this:
|
|
||||||
|
|
||||||
lang=txt
|
|
||||||
No library currently exists at that path...
|
|
||||||
The directory '/some/path/libcustom/src' does not exist.
|
|
||||||
|
|
||||||
Do you want to create it? [y/N] y
|
|
||||||
Creating new libphutil library in '/some/path/libcustom/src'.
|
|
||||||
Choose a name for the new library.
|
|
||||||
|
|
||||||
What do you want to name this library?
|
|
||||||
|
|
||||||
Choose a library name (in this case, "libcustom" would be appropriate) and it
|
|
||||||
you should get some details about the library initialization:
|
|
||||||
|
|
||||||
lang=txt
|
|
||||||
Writing '__phutil_library_init__.php' to
|
|
||||||
'/some/path/libcustom/src/__phutil_library_init__.php'...
|
|
||||||
Using library root at 'src'...
|
|
||||||
Mapping library...
|
|
||||||
Verifying library...
|
|
||||||
Finalizing library map...
|
|
||||||
OKAY Library updated.
|
|
||||||
|
|
||||||
This will write three files:
|
|
||||||
|
|
||||||
- `src/.phutil_module_cache` This is a cache which makes "arc liberate"
|
|
||||||
faster when you run it to update the library. You can safely remove it at
|
|
||||||
any time. If you check your library into version control, you can add this
|
|
||||||
file to ignore rules (like .gitignore).
|
|
||||||
- `src/__phutil_library_init__.php` This records the name of the library and
|
|
||||||
tells libphutil that a library exists here.
|
|
||||||
- `src/__phutil_library_map__.php` This is a map of all the symbols
|
|
||||||
(functions and classes) in the library, which allows them to be autoloaded
|
|
||||||
at runtime and dependencies to be statically managed by "arc liberate".
|
|
||||||
|
|
||||||
= Linking with Phabricator =
|
|
||||||
|
|
||||||
If you aren't using this library with Phabricator (e.g., you are only using it
|
|
||||||
with Arcanist or are building something else on libphutil) you can skip this
|
|
||||||
step.
|
|
||||||
|
|
||||||
But, if you intend to use this library with Phabricator, you need to define its
|
|
||||||
dependency on Phabricator by creating a `.arcconfig` file which points at
|
|
||||||
Phabricator. For example, you might write this file to
|
|
||||||
`libcustom/.arcconfig`:
|
|
||||||
|
|
||||||
{
|
|
||||||
"load": [
|
|
||||||
"phabricator/src/"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
For details on creating a `.arcconfig`, see
|
|
||||||
@{article:Arcanist User Guide: Configuring a New Project}. In general, this
|
|
||||||
tells `arc liberate` that it should look for symbols in Phabricator when
|
|
||||||
performing static analysis.
|
|
||||||
|
|
||||||
NOTE: If Phabricator isn't located next to your custom library, specify a
|
|
||||||
path which actually points to the `phabricator/` directory.
|
|
||||||
|
|
||||||
You do not need to declare dependencies on `arcanist` or `libphutil`,
|
|
||||||
since `arc liberate` automatically loads them.
|
|
||||||
|
|
||||||
Finally, edit your Phabricator config to tell it to load your library at
|
|
||||||
runtime, by adding it to `load-libraries`:
|
|
||||||
|
|
||||||
...
|
|
||||||
'load-libraries' => array(
|
|
||||||
'libcustom' => 'libcustom/src/',
|
|
||||||
),
|
|
||||||
...
|
|
||||||
|
|
||||||
Now, Phabricator will be able to load classes from your custom library.
|
|
||||||
|
|
||||||
= Writing Classes =
|
|
||||||
|
|
||||||
To actually write classes, create a new module and put code in it:
|
|
||||||
|
|
||||||
libcustom/ $ mkdir src/example/
|
|
||||||
libcustom/ $ nano src/example/ExampleClass.php # Edit some code.
|
|
||||||
|
|
||||||
Now, run `arc liberate` to regenerate the static resource map:
|
|
||||||
|
|
||||||
libcustom/ $ arc liberate src/
|
|
||||||
|
|
||||||
This will automatically regenerate the static map of the library.
|
|
||||||
|
|
||||||
= What You Can Extend And Invoke =
|
|
||||||
|
|
||||||
libphutil, Arcanist and Phabricator are strict about extensibility of classes
|
|
||||||
and visibility of methods and properties. Most classes are marked `final`, and
|
|
||||||
methods have the minimum required visibility (protected or private). The goal of
|
|
||||||
this strictness is to make it clear what you can safely extend, access, and
|
|
||||||
invoke, so your code will keep working as the upstream changes.
|
|
||||||
|
|
||||||
When developing libraries to work with libphutil, Arcanist and Phabricator, you
|
|
||||||
should respect method and property visibility and extend only classes marked
|
|
||||||
`@stable`. They are rendered with a large callout in the documentation (for
|
|
||||||
example: @{class@libphutil:AbstractDirectedGraph}). These classes are external
|
|
||||||
interfaces intended for extension.
|
|
||||||
|
|
||||||
If you want to extend a class but it is not marked `@stable`, here are some
|
|
||||||
approaches you can take:
|
|
||||||
|
|
||||||
- Good: If possible, use composition rather than extension to build your
|
|
||||||
feature.
|
|
||||||
- Good: Check the documentation for a better way to accomplish what you're
|
|
||||||
trying to do.
|
|
||||||
- Good: Let us know what your use case is so we can make the class tree more
|
|
||||||
flexible or configurable, or point you at the right way to do whatever
|
|
||||||
you're trying to do, or explain why we don't let you do it.
|
|
||||||
- Discouraged: Send us a patch removing "final" (or turning "protected" or
|
|
||||||
"private" into "public"). We generally will not accept these patches, unless
|
|
||||||
there's a good reason that the current behavior is wrong.
|
|
||||||
- Discouraged: Create an ad-hoc local fork and remove "final" in your copy of
|
|
||||||
the code. This will make it more difficult for you to upgrade in the future.
|
|
||||||
- Discouraged: Use Reflection to violate visibility keywords.
|
|
Loading…
Reference in a new issue