虽然只是phpBB开发小组针对团队合作开发所写的规范, 但是对于我们的PHP程序写作有普遍的指导意义, 本文由PHPBBCHINA的IOsetting翻译, 如果有差错和不妥之处, 请不吝指出.
Contents |
设定
编辑器设定
制表符和空格(Tabs vs Spaces):
简单起见, 在编写代码的时候我们将使用制表符(Tab)而不是空格(space). 我们设置一个制表符的宽度为四个空格大小. 您需要调整您的编辑器以适合这一定义. 请确认当您保存文件时, 不会将制表符替换为空格. 这样我们每个人都能让代码以我们喜欢的形式显示, 而不会破坏文件的实际布局.
在行首的制表符一般不会有问题, 但是在代码行中出现时, 如果您没有设置它为我们统一使用的空格数将使显示出现问题. 这里是一个简单的例子:
{TAB}$mode{TAB}{TAB}= request_var('mode', ); {TAB}$search_id{TAB}= request_var('search_id', );
如果输入制表符 (代码中的{TAB}), 两个等号应该在同一列上对齐.
换行:
请确认您的编辑器以UNIX的格式保存文件. 这意味着每一行以新行结束, 而不是一个win32上所用的 CR/LF 结合符号, 或者是苹果操作系统所用的其他符号. 任何正规的编辑器都应该支持这种格式, 但是不一定是默认保存格式, 所以请检查一下您的编辑器设置.
文件头
文件的标准文件头:
所有的phpBB程序文件必须以这个模板开始:
/** * * @package {PACKAGENAME} * @version $Id: $ * @copyright (c) 2006 phpBB Group * @license http://opensource.org/licenses/gpl-license.php GNU Public License * */
请查看文件位置一节得到正确的package名称
包含代码的文件:
这些文件您需要在文件头下放置一个空的注释, 以免文档生成器将文件头设置到代码段里去
/** * {HEADER} */ /** */ {CODE}
只包含函数的代码文件:
请不要忘记为函数标记注释 (特别是文件头部的第一个函数). 每个函数应该至少要有一个注释说明这个函数的行为. 对于更复杂的函数请同时为每一段代码做出注释
只包含类的代码文件:
请不要忘记为类做出注释. 函数类需要一个独立的 @package 定义, 这和文件头的package名称是一致的. 除去这以外, 前面所说的对于只包含函数的代码文件的要求同样适用于类和类的函数.
文件头之后, 不属于类或函数的代码:
这种情况下, 最好的处理方式是添加一个ignore注释, 防止文档生成器产生疑惑, 例如:
/** * {HEADER} */ /** * @ignore */ 小的代码片段, 大多数是一些定义或者if判断语句 /** * {DOCUMENTATION} */ 类 ...
文件位置
被多个页面调用的函数应该放到 functions.php, 只会被一个页面调用的函数, 应该放到调用它的那个页面去, 放置在页末, 或者放到相关模块的函数文件中去. 一些 /includes 文件夹中的文件包含特定模块用到的函数, 例如上传附件, 内容显示, 用户相关等等.
下面是定义的package, 新功能/函数应该放置到相关的文件/位置中去, 同时也要在文件中指定正确的package名称. package名称定义在下面的列表中:
- phpBB3
核心文件和未被指派到独立package的文件 - acm
/includes/acm, /includes/cache.php
缓存系统 - acp
/adm, /includes/acp, /includes/functions_admin.php
管理员控制面板 - dbal
/includes/db
数据库抽象层
基类: dbal- /includes/db/dbal.php
基类 DBAL, 定义整体框架和通用定义词 - /includes/db/firebird.php
Firebird/Interbase 数据库抽象层 - /includes/db/msssql.php
MSSQL 数据库抽象层 - /includes/db/mssql_odbc.php
MSSQL ODBC 数据库抽象层 - /includes/db/mysql.php
MySQL 数据库抽象层 MySQL 3.x/4.0.x - /includes/db/mysql4.php
MySQL4 数据库抽象层 MySQL 4.1.x/5.x - /includes/db/mysqli.php
MySQLi 数据库抽象层 - /includes/db/oracle.php
Oracle 数据库抽象层 - /includes/db/postgres.php
PostgreSQL 数据库抽象层 - /includes/db/sqlite.php
Sqlite 数据库抽象层
- /includes/db/dbal.php
- docs
/docs
phpBB 文档 - images
/images
所有和风格无关的全局图片文件 - install
/install
安装系统 - language
/language
所有的语言文件 - login
/includes/auth
登录认证插件 - VC
/includes/captcha
CAPTCHA 验证图片 - mcp
mcp.php, /includes/mcp, report.php
版主控制面板 - ucp
ucp.php, /includes/ucp
用户控制面板 - search
/includes/search, search.php
搜索系统 - styles
/styles, style.php
phpBB 界面/模板/风格主题/图片集
代码设计/规范
请注意这些规范适用于所有的php, html, javascript 和 css 文件.
变量/函数命名
我们不使用任何匈牙利命名规则. 我们大多数人认为匈牙利命名法是造成代码困惑的主要原因.
变量名:
变量名应该全部小写, 使用下划线分隔各个单词, 例如:
$current_user 是正确的写法, 而 $currentuser 和 $currentUser 是错误的.
变量名应该浅显易懂, 还必须简洁. 我们不希望看到一个句子那么长的变量名, 但是多输入一些字符总比让人看着猜半天要好.
循环变量:
这是唯一一处允许单字符的变量名出现的地方. 在这里, 外部循环的变量名应该总是$i, 其次的内循环应该是$j, 然后是$k, 等等. 如果循环变量使用的是已经存在的有含义的变量名, 那么请忽略这个规则, 例子:
for ($i = 0; $i < $outer_size; $i++) { for ($j = 0; $j < $inner_size; $j++) { foo($i, $j); } }
函数名:
函数名称应该浅显易懂. 我们不是在做C编程, 我们不希望写出的函数名像 "stristr()". 和变量名一样, 函数名由小写字符组成, 由下划线分隔不同的单词. 函数命名应该包含一个动词. 好的函数名例如 print_login_status(), get_user_data(), 等等.
函数参数:
参数命名使用变量命名同样的规则. 我们不希望看到这样的函数: do_stuff($a, $b, $c). 大多数情况下, 我们希望看到函数名和参数就能明白这个函数的用途和用法.
总结:
这些规则的出发点是不希望因为懒惰而破坏代码的清晰度. 尽管这样需要各人的编程风格做一些调整. print_login_status_for_a_given_user() 有点过头了, 以这个为例子 -- 这个函数命名为 print_user_login_status(), 或者 print_login_status() 会更好些.
特殊名称:
对于所有的表情, 单数情况使用 smiley, 复数情况使用 smilies.
代码格式
一定要使用括号:
这是因为懒惰不愿意多敲两个字符而破坏程序可读性的另一种案例. 即使某些代码结构体只有一行, 也不要忘记括号. 例如:
// 这些是错误的. if (condition) do_stuff(); if (condition) do_stuff(); while (condition) do_stuff(); for ($i = 0; $i < size; $i++) do_stuff($i);
// 这些是正确的. if (condition) { do_stuff(); } while (condition) { do_stuff(); } for ($i = 0; $i < size; $i++) { do_stuff(); }
在哪里放置括号:
这也是网络上争论不休的话题, 但是我们打算用一种一句话可以描述的风格: 括号总是独自一行. 闭合括号总是和起始括号对齐在同一列上. 例如:
if (condition) { while (condition2) { ... } } else { ... } for ($i = 0; $i < $size; $i++) { ... } while (condition) { ... } function do_stuff() { ... }
在符号间使用括号:
这是另一个简单的增进代码可读性的代码风格. 无论您在什么时候写一个表达式, 定义, 等等, 请在字符间留一个空格. 就像在写一篇英文文章一样. 在变量名和运算符中留一空格. 不要在起始括号后和结束括号前放置空格, 不要在分号和逗号前放置空格. 这些最好举例描述一下, 例如::
// 每一对都先显示一个错的, 再显示一个对的. $i=0; $i = 0; if($i<7) ... if ($i < 7) ... if ( ($i < 7)&&($j > 8) ) ... if ($i < 7 && $j > 8) ... do_stuff( $i, 'foo', $b ); do_stuff($i, 'foo', $b); for($i=0; $i<$size; $i++) ... for ($i = 0; $i < $size; $i++) ... $i=($j < $size)?0:1; $i = ($j < $size) ? 0 : 1;
运算符优先级:
您能背出PHP中所有运算符的优先级吗? 反正我是不行. 如果记不起来, 就不用去猜了. 请在程序中不要吝啬多用几个括号让各个运算符的执行顺序一目了然. 当然这不是说括号越多越好, 有时候一些单表达式就可以不用括号. 例如:
// 这样的结果是什么? 谁敢来猜猜? $bool = ($i < 7 && $j > 8 || $k == 4); // 这样您应该一眼就能看出我在干什么了. $bool = (($i < 7) && (($j < 8) || ($k == 4))); // 但是这样写会更好些, 因为看起来比较容易, 也不会混驳代码 $bool = ($i < 7 && ($j < 8 || $k == 4));
字符串表达:
在PHP中用两种不同的方式来表示一个字符串 - 单引号或者双引号. 主要的区别在于语法解释器会对双引号表示的字符串进行变量替换, 而不会处理单引号表示的字符串. 因此您应该总是使用单引号, 除非您真的需要在字符串中处理变量. 这样, 我们可以减少程序运行消耗, 因为语法解释器不需要每次多处理一大堆根本没有变量的字符串.
同样, 如果您在函数调用中使用了一个字符串变量作为参数, 您不需要将这个变量包含在引号里. 这会导致语法解释器多做好多无用功. 记住, 几乎所有双引号中的转义符对于单引号都是无效的. 您需要留意以上的规则, 但是有时候为了代码的可读性, 可以适当的破例. 例如:
// 错误 $str = "This is a really long string with no variables for the parser to find."; do_stuff("$str"); // 正确 $str = 'This is a really long string with no variables for the parser to find.'; do_stuff($str); // 有时候单引号不是那么合适 $post_url = $phpbb_root_path . 'posting.' . $phpEx . '?mode=' . $mode . '&start=' . $start; // 双引号有时候能让代码行更集中 $post_url = "{$phpbb_root_path}posting.$phpEx?mode=$mode&start=$start";
在SQL表达式中混合使用单双引号是允许的(要遵守SQL的格式规定), 其他情况下请尽量使用同一种引用方式, 大多数情况下请使用单引号.
数组键名:
在PHP中, 使用不经单引号包含的字符串作为数组键名是合法的, 但是我们不希望如此 -- 键名应该总是由单引号包含而避免引起混淆. 注意这是使用一个字符串, 而不是使用变量做键名的情况.例如:
// 错误 $foo = $assoc_array[blah]; // 正确 $foo = $assoc_array['blah']; // 错误 $foo = $assoc_array["$var"]; // 正确 $foo = $assoc_array[$var];
注释:
每一个复杂的函数应该都需要足够的注释来告诉其他的程序员这个函数的用处, 用法和思路. 在错误情况下函数的行为(和什么情况下会产生错误)都应该在注释中描述清楚.
需要说明的很重要的一方面是函数运行的前提, 或进行正确操作的条件. 任何程序员应该可以在查看程序的任何一部分的时候很容易明白程序在做什么.
避免在只有一两行的注释上使用 /* */ , 这种情况下请使用 //.
混用数字与布尔值:
请不要这样使用. 除了特殊情况以外, 请命名常数变量来定义字母. 一般地, 使用数字0来检查某个数组是否包含0个元素是没问题的. 但是不允许给一个数字定义一个特殊意义然后像字母一样到处使用. 这不便于阅读和维护. 常量true和false应该在判断真假时取代1和0 -- 即使它们确实是包含同样的值(但是并不是同样的类型!), 使用常数变量能更明显的看出实际的逻辑. 必要的时候对变量进行类型转换, 而不是依赖变量的值(PHP现在对类型转换控制非常松散, 这会导致一些不严谨的代码产生安全漏洞).
运算符缩写:
运算符缩写降低可阅读性的唯一例子就是递增和递减运算符 $i++ 和 $j-- . 这些运算符不应该套用在一个表达式中. 它们只能单独作为一行使用. 在表达式中套用会让您在调试时头痛不已, 例如:
// 错误 $array[++$i] = $j; $array[$i++] = $k; // 正确 $i++; $array[$i] = $j; $array[$i] = $k; $i++;
行内判断式:
行内条件判断应该只使用在非常简单的判断上. 并且, 这应该用于赋值而不是调用函数和其他更复杂的操作. 这种方式容易导致错误, 也使得代码非常难以阅读. 所以, 请不要为了节约打字的时间而到处使用它. 例如:
// 不好的用法 ($i < $size && $j > $size) ? do_stuff($foo) : do_stuff($bar); // 允许的情况 $min = ($i < $j) ? $i : $j;
不要使用未初始化的变量:
在phpBB3中, 我们使用了严格的运行时错误报告. 这将意味着未初始化的变量将会产生warning. 使用isset()函数检测变量是否存在能尽量避免这些warning. 但是更可能的是变量总是存在的. 检测一个变量是否有效的方法很简单, 例如:
// 错误 if ($forum) ... // 正确 if (isset($forum)) ... // 正确 if (isset($forum) && $forum == 5)
empty()函数在检查一个变量是否存在或者是否为空的时候非常有用(一个空的字符串, 值为0的数字或字符串变量,NULL, false, 一个空数组或者一个已经声明但是没有值的变量). 因此可以用于替换isset($array) && sizeof($array) > 0 - 这可以简单的写成!empty($array).
Switch表达式:
Switch/case代码段有时候会比较长. 请注意代码的推进层次, 这样能让代码更加易懂. 例如:
// 错误 switch ($mode) { case 'mode1': // I am doing something here break; case 'mode2': // I am doing something completely different here break; } // 正确 switch ($mode) { case 'mode1': // I am doing something here break; case 'mode2': // I am doing something completely different here break; default: // Always assume that the case got not catched break; } // 这样也正确, 如果您在case和break中包含了很多代码 switch ($mode) { case 'mode1': // I am doing something here break; case 'mode2': // I am doing something completely different here break; default: // Always assume that the case got not catched break; }
即使可以不需要default段, 还是尽量加入default, 以让人明白代码段结束而避免产生误解.
如果因为流程需要不加入break, 请加入一句注释说明情况, 例如:
// Example with no break switch ($mode) { case 'mode1': // I am doing something here // no break here case 'mode2': // I am doing something completely different here break; default: // Always assume that the case got not catched break; }
SQL格式
SQL 整体规范:
所有的SQL必须能跨DB兼容, 如果使用了特定DB的SQL, 必须同时提供支持其他数据库的脚本(MySQL3/4/5, MSSQL (7.0 and 2000), PostgreSQL (7.0+), Firebird, SQLite, Oracle8, ODBC (generalised if possible)).
所有SQL命令必须于数据库抽象层(DBAL)
SQL 脚本格式:
SQL 代码常常会变得很长, 如果不作一定的格式规范, 将很难读懂. SQL代码一般按照以下的格式书写, 以关键字换行:
$sql = 'SELECT * <-one tab->FROM ' . SOME_TABLE . ' <-one tab->WHERE a = 1 <-two tabs->AND (b = 2 <-three tabs->OR b = 3) <-one tab->ORDER BY b';
这里是应用了制表符后的例子:
$sql = 'SELECT * FROM ' . SOME_TABLE . ' WHERE a = 1 AND (b = 2 OR b = 3) ORDER BY b';
SQL 字符串:
在合适的地方使用双引号... 例如:
// 错误的 "UPDATE " . SOME_TABLE . " SET something = something_else WHERE a = $b"; 'UPDATE ' . SOME_TABLE . ' SET something = ' . $user_id . ' WHERE a = ' . $something; // 正确的 'UPDATE ' . SOME_TABLE . " SET something = something_else WHERE a = $b"; 'UPDATE ' . SOME_TABLE . " SET something = $user_id WHERE a = $something";
另一方面在没有变量的地方请使用单引号, 否则使用双引号.
DBAL一般函数:
sql_escape():
如果您需要往一段SQL代码内的字符串, 请使用$db->sql_escape() (即使您确信变量不会包含单引号, 也不要因为自信而导致错误), 例如:
$sql = 'SELECT * FROM ' . SOME_TABLE . " WHERE username = '" . $db->sql_escape($username) . "'";
sql_query_limit():
我们不直接往SQL语句中添加Limit字段, 而使用$db->sql_query_limit(). 您只需要传递查询语句和返回结果的起始点和记录条数.
注意: 因为Oracle处理limit的方式不同和我们为实现这种处理方式而使用的方法, 您在使用sql_query_limit从多个表单中取回数据时需要特别小心.
请注意在x中没有jars这一项时使用 "SELECT x.*, y.jars" 这样的语句格式; 确认内项和外项没有重叠的地方.
sql_build_array():
如果您需要UPDATE或INSERT数据, 请使用$db->sql_build_array() 函数. 这个函数会自动处理字符串并检查其他类型, 所以不必在写相应的代码. 需要插入的数据应该是一个数组 - $sql_ary - 或直接直接包含在语句中, 如果需要insert或update一两个变量. 一个insert语句的例子:
$sql_ary = array( 'somedata' => $my_string, 'otherdata' => $an_int, 'moredata' => $another_int ); $db->sql_query('INSERT INTO ' . SOME_TABLE . ' ' . $db->sql_build_array('INSERT', $sql_ary));
还有一个update的语句:
$sql_ary = array( 'somedata' => $my_string, 'otherdata' => $an_int, 'moredata' => $another_int ); $sql = 'UPDATE ' . SOME_TABLE . ' SET ' . $db->sql_build_array('UPDATE', $sql_ary) . ' WHERE user_id = ' . (int) $user_id; $db->sql_query($sql);
$db->sql_build_array() 函数支持这些模式: INSERT (例子如上), INSERT_SELECT (为 INSERT INTO table (...) SELECT value, column ... 这样的语句建立查询), MULTI_INSERT (返回扩展的插入), UPDATE (例子如上) 和SELECT (建立 WHERE 语句 [AND logic]).
sql_in_set():
$db->sql_in_set() 函数用于构造 IN () and NOT IN () 结构. 因为 (特别是) MySQL 如果在比较一个值的时候使用 = 或 <> 会具有更高效率, , 我们让 DBAL 决定如何使用. 一个典型的比较数值的例子是:
$sql = 'SELECT * FROM ' . FORUMS_TABLE . ' WHERE ' . $db->sql_in_set('forum_id', $forum_ids); $db->sql_query($sql);
$forum_ids内值的个数不同, 查询也会变得不一样.
// SQL语句: if $forum_ids = array(1, 2, 3); SELECT FROM phpbb_forums WHERE forum_id IN (1, 2, 3) // SQL 语句: if $forum_ids = array(1) or $forum_ids = 1 SELECT FROM phpbb_forums WHERE forum_id = 1
同样也可以反匹配negative match多个数值:
$sql = 'SELECT * FROM ' . FORUMS_TABLE . ' WHERE ' . $db->sql_in_set('forum_id', $forum_ids, true); $db->sql_query($sql);
$forum_ids内值的个数不同, 查询也会变得不一样.
// SQL 语句: if $forum_ids = array(1, 2, 3); SELECT FROM phpbb_forums WHERE forum_id NOT IN (1, 2, 3)
// SQL 语句: if $forum_ids = array(1) or $forum_ids = 1 SELECT FROM phpbb_forums WHERE forum_id <> 1
如果给的值为空, 会产生一个错误.
sql_build_query():
$db->sql_build_query() 函数负责建立select和select distinct查询语句, 如果您需要JOIN更多的table或从中得到更多的数据. 需要注意的是必须确保生成的语句在支持的数据库上都能正常工作. 一个简单的例子是:
$sql_array = array( 'SELECT' => 'f.*, ft.mark_time', 'FROM' => array( FORUMS_WATCH_TABLE => 'fw', FORUMS_TABLE => 'f' ), 'LEFT_JOIN' => array( array( 'FROM' => array(FORUMS_TRACK_TABLE => 'ft'), 'ON' => 'ft.user_id = ' . $user->data['user_id'] . ' AND ft.forum_id = f.forum_id' ) ), 'WHERE' => 'fw.user_id = ' . $user->data['user_id'] . ' AND f.forum_id = fw.forum_id', 'ORDER_BY' => 'left_id' ); $sql = $db->sql_build_query('SELECT', $sql_array);
sql_build_query() 的第一个参数可能是SELECT或者SELECT_DISTINCT. 就像您看到的那样, 逻辑非常容易理解. LEFT_JOIN关键字为例子中的table添加了另一个数组. 使用这种结构添加的好处是您可以轻易地使用条件判断来建立查询语句. 例如上面只需要在服务器端主题跟踪启用的情况下才启用左连接; 一个简单的判断语句:
$sql_array = array( 'SELECT' => 'f.*', 'FROM' => array( FORUMS_WATCH_TABLE => 'fw', FORUMS_TABLE => 'f' ), 'WHERE' => 'fw.user_id = ' . $user->data['user_id'] . ' AND f.forum_id = fw.forum_id', 'ORDER_BY' => 'left_id' ); if ($config['load_db_lastread']) { $sql_array['LEFT_JOIN'] = array( array( 'FROM' => array(FORUMS_TRACK_TABLE => 'ft'), 'ON' => 'ft.user_id = ' . $user->data['user_id'] . ' AND ft.forum_id = f.forum_id' ) ); $sql_array['SELECT'] .= ', ft.mark_time '; } else { // Here we read the cookie data } $sql = $db->sql_build_query('SELECT', $sql_array);
优化
循环定义中的操作:
在比较部分如果存在运算请一定要进行优化. 因为这部分会在循环中的每一步进行操作:
// 在每次循环中sizeof函数都要被调用 for ($i = 0; $i < sizeof($post_data); $i++) { do_something(); } // 您可以在循环起始部分对这个不变的量赋值 for ($i = 0, $size = sizeof($post_data); $i < $size; $i++) { do_something(); }
in_array()用法:
避免在大的数组上使用 in_array(), 同时避免在循环中对包含20个以上元素的数组使用这个函数. in_array()会非常消耗资源. 对于小的数组这种影响可能很小, 但是在一个循环中检查大数组可能会需要好几秒钟的时间. 如果您确实需要这个功能, 请使用isset()来查找数组元素. 实际上是使用键名来查询键值. 调用 isset($array[$var]) 会比 in_array($var, array_keys($array)) 要快得多.
总体规范
基本规则:
- 不要信任用户输入 (这对服务器端变量也是一样, 比如cookie).
- 对函数返回的值进行净化.
- 在函数中对函数参数进行净化.
- 在所有的身份检查中都要使用auth类.
- 不要删除任何版权信息 (包含在代码中的, 或者在运行/编译时显示的), 也不要对版权信息进行任何形式的修改 (添加是允许的).
变量:
使用request_var()函数检查所有的参数(除去提交的和single checking参数.
request_var() 函数根据第二个参数决定数据类型(同时决定了默认值). 如果您需要一个标量变量, 您需要明确地告诉request_var函数. 例如:
// 旧的方式, 不要再使用 $start = (isset($HTTP_GET_VARS['start'])) ? intval($HTTP_GET_VARS['start']) : intval($HTTP_POST_VARS['start']); $submit = (isset($HTTP_POST_VARS['submit'])) ? true : false; // 使用request var 并定义默认值 (使用正确的类型) $start = request_var('start', 0); $submit = (isset($_POST['submit'])) ? true : false; // $start 是int变量, 如下的形式是错误的 $start = request_var('start', '0'); // 获取一个数组, 键名是整数, 默认值是0 $mark_array = request_var('mark', array(0)); // 获取一个数组, 键名是字符串,默认值是0 $action_ary = request_var('action', array( => 0));
登录验证/重定向:
要显示一个登录框登录论坛, 请使用 login_forum_box($forum_data), 否则使用 login_box() 函数.
login_box() 函数可以使用重定向目标为第一个参数. 作为一个默认规则, 如果需要用户返回当前页面请指定一个空的字符串. 此外不要在重定向字符串中添加$SID(例如我们在用户控制面板登录中重定向为论坛首页, 因为如果不这么做, 用户将又被带回登录框界面).
确认操作:
对于一些重要和敏感的操作, 需要用户确认后再执行. 使用confirm_box()函数来构建确认框.
对话:
必须在每一页初始化对话, 在最顶部使用如下的代码:
$user->session_begin(); $auth->acl($user->data); $user->setup();
$user->setup()函数可以用于传递额外的语言和自定义风格(在viewfroum时使用).
错误和提示:
所有的信息和错误必须使用trigger_error()输出, 同时要指定合适的消息类型和语言. 例如:
trigger_error('NO_FORUM'); trigger_error($user->lang['NO_FORUM']); trigger_error('NO_MODE', E_USER_ERROR);
URL格式化
所有指向内部文件的URL必须由 $phpbb_root_path. 所有指向管理员控制面板内部文件的URL必须以 $phpbb_admin_path 开头. 这样能确保路径不会出错, 用户可以修改adm目录名, 也不会导致链接失效(不过需要少量修改代码才能正常工作).
2.0.x的append_sid()函数依然可用, 虽然不自动处理URL变化. 可以查看代码文档得到更多关于这个函数的细节. 一个使用的例子:
append_sid("{$phpbb_root_path}memberlist.$phpEx", 'mode=group&g=' . $row['group_id'])
其他:
选择这些函数仅仅是因为个人喜好, 对代码的可靠性没有太多关系.
- 使用 sizeof 替代 count
- 使用 strpos 替代 strstr
- 使用 else if 替代 elseif
- 使用 false (lowercase) 替代 FALSE
- 使用 true (lowercase) 替代 TRUE
风格
基本规则
模板应该带给用户统一稳定的感受. 比较妥当的实现方式是在一个已经存在的版本基础上修改, 例如index, viewforum或者viewtopic(这些模板都是一系列表单, 变量的组合实现). 需要注意的是编程规范也应该尽可能有的应用在模板制作上.
外围的talbe类forumline已经取消并用tablebg替换.
当使用 <table> 时, 需要按这个格式书写 <table class="" cellspacing="" cellpadding="" border="" align=""> 便于生成表单类型的结构, 也易于调整属性.
每一级的单元格必须要增加一个制表符缩进, 例如<tr> <td> , 而 <table> 的缩进和随后(结尾)的 <tr> 应该在同一行. 这种格式同样适用于div元素.
除非非常必要的情况请尽量少用 <span> 标签 ... CSS 中文字的大小依靠父类定义. 所以使用 <span class="gensmall"><span class="gensmall">TEST</span></span> 将会得到非常小的文字. 同样的, 当另一个元素可以包含类定义时也不要使用span, 例如.
<td><span class="gensmall">TEST</span></td>
就可以写成:
<td class="gensmall">TEST</td>
尽量使用相近的用法匹配文字类型, 例如不要在viewtopic的小字体上使用导航条的类型.
行的颜色和类型由模板定义, 使用 IF S_ROW_COUNT 来切换不同的类型, 可以查看 viewtopic 或 viewforum 中的例子.
注意码块的级别顺序很重要, 即使不是所有的页面都能通过严格兼容XHTML 1.0的验证, 这也是我们尽力达到的目标.
在外层table上使用标准的cellpadding = 2 和 cellspacing = 0. 内层table可以根据需要使用0到3甚至4.
使用 div + css 做风格设计, 而使用table做数据展示.
分开的 catXXXX 和 thXXX 类型不再存在. 当定义表格头部时请使用 <th> 而不要使用 <th class="thHead">. 相似的还有cat, 不要使用 <td class="catLeft">, 而要使用<td class="cat">.
尽量保持基础布局和风格定义的一致性. 例如 _EXPLAIN 文本一般放置在其解释的标题下. 例如{L_POST_USERNAME}<br /><span class="gensmall">{L_POST_USERNAME_EXPLAIN}</span> 就是典型的例子 ... 当然也会有例外, 这不是一个强制性的规定.
在写代码的过程中尽量保持条件语句中的模板语句前头的缩进.
这样是正确的:
<tr> <td>{test.TEXT}</td> </tr>
这样写也可以:
<tr> <td>{test.TEXT}</td> </tr>
这可以让阅读代码的人能立即知道循环内的语句是什么, 判断条件是什么.
模板
文件命名
首先现在的模板使用了".html"后缀而不再使用".tpl"后缀. 这样可以方便一些人在阅读的时候得到语法高亮.
变量命名
所有的模板变量都应该妥当的命名(使用下划线连接多个单词), 语系相关的变量需要使用前缀 L_, 系统数据使用 S_, URL使用 U_, javascript URL使用 UA_, 要输入 javascript 语句的语系变量要用 LA_, 其他变量可以自行定义.
L_* 这样的模板变量在代码没有对其赋值时会自动被映射到相应的语系变量上. 例如 {L_USERNAME} 会被映射到 $user->lang['USERNAME']. 而 LA_* 也会被同样处理, 但是会被转义后输入到 javascript 代码. 这样的机制将减少加载新语系时需要的修改.
代码块/循环
基本的代码块循环使用这样的形式:
<!-- BEGIN loopname --> markup, {loopname.X_YYYYY}, etc. <!-- END loopname -->
后面会更进一步讲解循环, 先别着急, 我们先解释一下条件和其他声明.
文件包含
有些2.0.x的特性在3.0.x不再延续, 例如将模板赋值给一个变量. 这在2.0.x中用于输出版面跳转的下拉框. 在3.0.x中, 我们使用 INCLUDE 声明来实现这个功能. 方法是
<!-- INCLUDE filename -->
您会注意到3.0.x中模板大多是以 或 开头的. 而在 2.0.x 中是由程序中的逻辑来判断使用哪种模板做页首. 在 3.0.x 中模板设计者可以输出任何想要的模板. 值得一提的是你可以任意引入新模板 (而不仅仅是默认的那些) 而不需要像2.0.x那样修改程序中的加载项 ... 对于一些全站的菜单, 导航条什么的是很有用的.
PHP代码段
一个有争议的特性是在模板中引入PHP代码. 这可以通过在代码中写入如下格式的语句实现:
<!-- PHP --> echo "hello!"; <!-- ENDPHP -->
您也可以引入一个外部的PHP文件:
<!-- INCLUDEPHP somefile.php -->
它将被包含并被执行.
不过, 我们还是建议设计者不要将PHP代码引入模板. 这个功能主要用来允许用户包含页首代码而不需要修改太多代码, 就像在2.0.x里面那样. 这个功能不能滥用. 所以 www.phpbb.com 将不会制作或提供包含PHP的模板. 默认情况下, 模板中的PHP是被禁用的(如果需要启用的话, 必须由管理员专门指定).
条件/结构控制
在3.0.x中最显著的增强在于条件控制语句. "if 条件1 then 动作1 else 动作2". 系统与Smarty非常类似. 这可能会让一些人开头比较困惑, 但是它确实提供了更多的灵活度. 最常见的结构莫过于:
<!-- IF expr --> markup <!-- ENDIF -->
表达式也可以是多种形式, 例如:
<!-- IF loop.S_ROW_COUNT is even --> markup <!-- ENDIF -->
这将在当前循环中S_ROW_COUNT为偶数时输出markup, 例如当值为TRUE的时候. 您可以使用多种比较形式 (标准符号或者在括号中等价的文字) 包含 (not, or, and, eq, neq, 可以尽量多使用这些以提高代码的可读性):
== [eq] != [neq, ne] <> (same as !=) !== (not equivalent in value and type) === (equivalent in value and type) > [gt] < [lt] >= [gte] <= [lte] && [and] || [or] % [mod] ! [not] + - * / , << (bitwise shift left) >> (bitwise shift right) | (bitwise or) ^ (bitwise xor) & (bitwise and) ~ (bitwise not) is (can be used to join comparison operations)
基本的插入语也被用于增强旧的 BODMAS 规则. 另外还有一些基本点饿判断类型:
even odd div
另外简单的IF语句也可以用来做一系列的比较, 例如:
<!-- IF expr1 --> markup <!-- ELSEIF expr2 --> markup . . . <!-- ELSEIF exprN --> markup <!-- ELSE --> markup <!-- ENDIF -->
每个表达式将依次被测试并输出满足条件的那一组模板.
好吧让我们看看我们能用这些功能实现些什么, 首先是在viewforum时对每行进行着色. 在 2.0.x 中着色的区别在程序中指定为 row color1, row color2 或者 row class1, row class2. 在 3.0.x 中这个逻辑被放到了模板中处理, 一开始可能会让您觉得不太对头但是这是非常简化的做法:
<table> <!-- IF loop.S_ROW_COUNT is even --> <tr class="row1"> <!-- ELSE --> <tr class="row2"> <!-- ENDIF --> <td>HELLO!</td> </tr> </table>
这将使每行的背景根据奇偶数而交替变化. S_ROW_COUNT 是循环中赋值的默认参数. 另一个例子是:
<table> <!-- IF loop.S_ROW_COUNT > 10 --> <tr bgcolor="#FF0000"> <!-- ELSEIF loop.S_ROW_COUNT > 5 --> <tr bgcolor="#00FF00"> <!-- ELSEIF loop.S_ROW_COUNT > 2 --> <tr bgcolor="#0000FF"> <!-- ELSE --> <tr bgcolor="#FF00FF"> <!-- ENDIF --> <td>hello!</td> </tr> </table>
这将在前两行输出紫色, 第三到第五行输出蓝色, 第六到第十行输出绿色, 剩下的输出红色. 看到这种控制方法的方便之处了吗?
还有其他的用处吗? 是的, 您可以用 IF 做一些简单的判断, 例如用户的登录状态:
<!-- IF S_USER_LOGGED_IN --> markup <!-- ENDIF -->
这替代了2.0.x中使用空数组和 BEGIN/END 的方法.
代码块/循环的扩展语法
回到我们的循环语句 - 它们有一些增强和扩展. 首先您可以在循环中设定起始和结束点. 例如:
<!-- BEGIN loopname(2) --> markup <!-- END loopname -->
将在第三个循环中开始(假设循环计数从0开始). 例如:
loopname(2): 在第三个循环开始
loopname(-2): 在倒数第二个循环开始
loopname(3,4): 在循环的第四个开始, 到第五个结束
loopname(3,-4): 在循环的第四个开始, 在倒数第四个结束
另一个增强是 BEGINELSE:
<!-- BEGIN loop --> markup <!-- BEGINELSE --> markup <!-- END loop -->
档循环包含空值时就会输出 BEGINELSE 和 END 之间的内容. 对于一些情况特别有用 (比如空的版面) ... 某些角度上讲这中功能替换了一些 "switch_" 类型的判断控制.
另一个用于判断空循环的方法是在循环名的前面加上一个点:
<!-- IF .loop --> <!-- BEGIN loop --> markup <!-- END loop --> <!-- ELSE --> markup <!-- ENDIF -->
您甚至可以检查一个循环的循环个数:
<!-- IF .loop > 2 --> <!-- BEGIN loop --> markup <!-- END loop --> <!-- ELSE --> markup <!-- ENDIF -->
嵌套的循环使得条件判断必须加上各个循环的前缀. 例如:
<!-- BEGIN firstloop --> {firstloop.MY_VARIABLE_FROM_FIRSTLOOP} <!-- BEGIN secondloop --> {firstloop.secondloop.MY_VARIABLE_FROM_SECONDLOOP} <!-- END secondloop --> <!-- END firstloop -->
有时候有必要结束一些嵌套的循环而在外循环中调用另外一个循环. 这听起来有点晕不过没关系, 这很少用到. 下面这个有点复杂的例子演示了这个功能 - 它还教会你如何检测循环的第一个和最后一个循环 (后面我会更详细一些解释这个例子):
<!-- BEGIN l_block1 --> <!-- IF l_block1.S_SELECTED --> <strong>{l_block1.L_TITLE}</strong> <!-- IF S_PRIVMSGS --> <!-- the ! at the beginning of the loop name forces the loop to be not a nested one of l_block1 --> <!-- BEGIN !folder --> <!-- IF folder.S_FIRST_ROW --> <ul class="nav"> <!-- ENDIF --> <li><a href="{folder.U_FOLDER}">{folder.FOLDER_NAME}</a></li> <!-- IF folder.S_LAST_ROW --> </ul> <!-- ENDIF --> <!-- END !folder --> <!-- ENDIF --> <ul class="nav"> <!-- BEGIN l_block2 --> <li> <!-- IF l_block1.l_block2.S_SELECTED --> <strong>{l_block1.l_block2.L_TITLE}</strong> <!-- ELSE --> <a href="{l_block1.l_block2.U_TITLE}">{l_block1.l_block2.L_TITLE}</a> <!-- ENDIF --> </li> <!-- END l_block2 --> </ul> <!-- ELSE --> <a class="nav" href="{l_block1.U_TITLE}">{l_block1.L_TITLE}</a> <!-- ENDIF --> <!-- END l_block1 -->
首先让我们看看这段代码:
<!-- BEGIN l_block1 --> <!-- IF l_block1.S_SELECTED --> markup <!-- ELSE --> <a class="nav" href="{l_block1.U_TITLE}">{l_block1.L_TITLE}</a> <!-- ENDIF --> <!-- END l_block1 -->
这里我们开始循环 l_block1 并且在循环中当变量 S_SELECTED 的值为真的时候输出一些东西, , 否则我们输出一个带链接的标题. 这里, 您看到引用了 {l_block1.L_TITLE}, 您知道 L_* 这样的变量会被自动用相应语系的翻译赋值, 但是在循环中带了前缀后就不是这样, 它们需要由循环 l_block1 在程序中赋值.
让我们再仔细看看这段模板:
<!-- BEGIN l_block1 --> . . <!-- IF S_PRIVMSGS --> <!-- BEGIN !folder --> <!-- IF folder.S_FIRST_ROW --> <ul class="nav"> <!-- ENDIF --> <li><a href="{folder.U_FOLDER}">{folder.FOLDER_NAME}</a></li> <!-- IF folder.S_LAST_ROW --> </ul> <!-- ENDIF --> <!-- END !folder --> <!-- ENDIF --> . . <!-- END l_block1 -->
语句检查一个全局变量, 当S_PRIVMSGS为真时进入 语句段. 这是一个循环. l_block2的情形和l_block1相似, 但是有l_block1的参与:
<!-- BEGIN l_block1 --> . . <ul class="nav"> <!-- BEGIN l_block2 --> <li> <!-- IF l_block1.l_block2.S_SELECTED --> <strong>{l_block1.l_block2.L_TITLE}</strong> <!-- ELSE --> <a href="{l_block1.l_block2.U_TITLE}">{l_block1.l_block2.L_TITLE}</a> <!-- ENDIF --> </li> <!-- END l_block2 --> </ul> . . <!-- END l_block1 -->
发现了吗? 循环 l_block2 是 l_block1 的一个子循环, 但是循环folder是主循环. 回到 folder 循环:
<!-- IF folder.S_FIRST_ROW --> <ul class="nav"> <!-- ENDIF --> <li><a href="{folder.U_FOLDER}">{folder.FOLDER_NAME}</a></li> <!-- IF folder.S_LAST_ROW --> </ul> <!-- ENDIF -->
您可能问 S_FIRST_ROW 和 S_LAST_ROW 变量代表什么. 从字面上就可以猜到 - 这是用来表示循环的第一次执行和最后一次执行. 在做页面设计的时候会经常需要用到这两个变量来打开或关闭页面元素. 让我们想象一下一个使用这些的循环, 例如:
<ul class="nav"> <!-- written on first iteration --> <li>first element</li> <!-- written on first iteration --> <li>second element</li> <!-- written on second iteration --> <li>third element</li> <!-- written on third iteration --> </ul> <!-- written on third iteration -->
就像你看到的, 三个element按照顺序显示. 有时候你会需要屏蔽某些元素的输出, 例如:
<!-- IF folder.S_FIRST_ROW --> <ul class="nav"> <!-- ELSEIF folder.S_LAST_ROW --> </ul> <!-- ELSE --> <li><a href="{folder.U_FOLDER}">{folder.FOLDER_NAME}</a></li> <!-- ENDIF -->
会输出下面的页面代码:
<ul class="nav"> <!-- written on first iteration --> <li>second element</li> <!-- written on second iteration --> </ul> <!-- written on third iteration -->
需要注意的是处理都是自上而下执行的.
翻译(i18n/L10n)规范
标准化
缘由:
phpBB是世界上不同语系版本最多的项目之一, 已经有超过60个语系版本. 在相互独立的语系命名和翻译中, phpBB3已经超乎想象地保证了过程的统一和有序, 为与当今和将来的浏览器协调而努力.
编码:
phpBB3中, 输出编码为 UTF-8, 一个世界通用的编码标准, 由Unicode Consortium提出, 目的在于产生一个包容 US-ASCII 和ISO-8859-1 的并集. 在过去, 支持所有语系意味着需要支持不同的编码 (例如: ISO-8859-1 到 ISO-8859-15 (拉丁语, 希腊语, 斯拉夫语, 泰语, 希伯来语, 阿拉伯语); GB2312 (简体中文); Big5 (繁体中文), EUC-JP (日语), EUC-KR (韩语), VISCII (越南语); 等等), 使用UTF-8就不再需要转换不同的编码, 使得多语种并存的论坛变得更加容易实现.
影响之一是语言文件必须使用UTF-8编码, 而且出于兼容的考虑, 不能在文件头包含 BOM. 那些使用拉丁字符的论坛 (例如大部分的欧洲论坛), 这种变化是透明的, 因为 UTF-8 是 US-ASCII 和 ISO-8859-1的并集.
语系:
IETF 最近发布了RFC 4646用于定义和区分语系, 结合RFC 4647 obseletes 以前的RFC 3006 和更早的 RFC 1766. RFC 4646 使用ISO 639-1/ISO 639-2, ISO 3166-1 alpha-2, ISO 15924 和 UN M.49 来定义一个语系. 每个完整的语系标签由下面子标签组成, 不区分大小写, 子标签可以为空.
按字母排序非空情况下的子标签是: language-script-region-variant-extension-privateuse. 当某个子标签为空时, 相应的连字符也会删除. 这样英文的语系标签会是 en 而不是 en-----.
大多数语系标签包含一个双字符或三字符的语系子标签 (定义自ISO 639-1/ISO 639-2). 有时也会在开头有一个两位或三位数字代表地区的子标签 (定义自 ISO 3166-1 alpha-2 或 UN M.49). 例如:
语系标签 | 说明 | 子标签结构 |
---|---|---|
en | 英文 | 语言 |
mas | 马赛语 | 语言 |
fr-CA | 使用于加拿大的法语 | 语言+地区 |
en-833 | 使用于马恩岛的英语 | 语言+地区 |
zh-Hans | 以简体书写的中文 | 语言+书写 |
zh-Hant-HK | 使用于香港, 繁体书写的中文 | 语言+书写+地区 |
de-AT-1996 | 1996年颁布的使用于奥地利的标准德语 | 语言+地区+变种 |
语言标签的最终目标是要能在字面上保留语言的区分信息, 同时还要尽可能的简短. 所以打个比方, 我们使用en, fr 和 ja 替代 en-GB, fr-FR 和 ja-JP, 因为我们知道英语, 法语和日语分别是不列颠, 法兰西和日本的本土语言.
另一个是 ISO 15924 语言书写体以及何时该注明/不该注明书写体. 例如, en-Latn 表示使用拉丁文书写的英文, 而现实世界中英文书写或多或少会出现一些拉丁文专有词. 对于英语这样使用单一的书写方式的语言, IANA Language Subtag Registry使用"Suppress-Script"项来表示不需注明书写体, 除非特定的语言标签需要特定的书写体. 某些语言使用多种书写体, 在这种情况下我们应该加上书写体标签, 因为某些读者可能会无法阅读其他书写体所表示的语言. 一些例子:
语言标签 | 简介 | 子标签组成 |
---|---|---|
en-Brai | 使用盲文书写的英文 | 语言+书写体 |
en-Dsrt | 使用Deseret (Mormon)书写体表示的英文 | 语言+书写体 |
sr-Latn | 拉丁文书写的塞尔维亚语 | 语言+书写体 |
sr-Cyrl | 古斯拉夫体书写的塞尔维亚语 | 语言+书写体 |
mn-Mong | 蒙古文书写的蒙古语 | 语言+书写体 |
mn-Cyrl | 古斯拉夫体书写的蒙古语 | 语言+书写体 |
mn-Phag | 以Phags-pa体书写的蒙古语(Phags-pa是忽必烈时代的蒙古国师) | 语言+书写体 |
az-Cyrl-AZ | 在阿塞拜疆使用的古斯拉夫体书写的阿塞拜疆语 | 语言+书写体+地区 |
az-Latn-AZ | 在阿塞拜疆使用的拉丁文书写的阿塞拜疆语 | 语言+书写体+地区 |
az-Arab-IR | 在伊朗使用的阿拉伯文书写的阿塞拜疆语 | 语言+书写体+地区 |
在宏观区域性实体存在或者在ISO 3166-1 alpha-2表示法含糊不清的情况下, 使用三位UN M.49数字和两位ISO 3166-1 alpha-2字符表示.
使用英文的宏观区域性实体例子:
ISO 639-1/ISO 639-2 + ISO 3166-1 alpha-2 | ISO 639-1/ISO 639-2 + UN M.49 (地区实例) | |
en-AU 澳洲英语 | en-053 使用于澳洲和新西兰的英语 | en-009 使用于大洋洲的英语 |
en-NZ 使用于新西兰的英语 | ||
en-FJ 使用于斐济的英语 | en-054 使用于美拉尼西亚群岛的英语 |
广义语种和分支:
RFC 4646 预期的一些特性在ISO 639-3 中将会生效, 这个标准目的在于尽可能地提供一个完整的的语言种类描述, 包括现存的, 已消失的, 古代的和构造的语种, 无论是主要次要还是无书写体的语种. ISO 639-3中区别于前两版的一个特性是广义语种的概念. 两个例子是阿拉伯语和汉语. 他们的语种缩写 ar 和 zh 无法准确描述使用的是何种方言, 或者是某些只有非常有学问的学者才能阅读的变体. 对于这些广义语种, 我们建议使用子语种标签作为后缀, 例如:
语言标签 | 简介 | 标签组成 |
zh-cmn | 普通话/国语 | macrolanguage+sublanguage |
zh-yue | 广东话/粤语 | macrolanguage+sublanguage |
zh-cmn-Hans | 普通话/国语, 简体 | macrolanguage+sublanguage+script |
zh-cmn-Hant | 普通话/国语, 繁体 | macrolanguage+sublanguage+script |
zh-nan-Latn-TW | 闽南话, 拉丁字符, 使用于台湾地区 | macrolanguage+sublanguage+script+region |
其他需要考虑的事项
phpBB语言标签标准化:
对于phpBB, 语言标签被转换为小写并且在合适的位置将短划-替换为下划线_, 例如:
语言标签 | 描述 | USER_LANG在 ./common.php 中的值 | 在/language/中的语系档名 |
---|---|---|---|
en | British English | en | en |
de-AT | German as used in Austria | de-at | de_at |
es-419 | Spanish as used in Latin America & Caribbean | en-419 | en_419 |
zh-yue-Hant-HK | Cantonese written in Traditional script as used in Hong Kong | zh-yue-hant-hk | zh_yue_hant_hk |
如何使用 iso.txt:
iso.txt文件是一个UTF-8编码的纯文本文件, 包含下面三行:
- 语言的英文名称
- 语言的当地语言名称
- 作者信息
iso.txt由phpBB.com的语系提交系统自动生成. 您如果打算在phpBB.com上发布这个语系档, 您不需要自己写这个文件, 不过需要记住phpBB在安装语系时需要这个文件.
因为语言标签自身是为机器识别设计, 所以不容易为人直观的阅读, 这也是为什么我们需要用iso.txt来分别说明各个语系档. 例如我们可以很容易的推测 en-US 是使用于美国的英语, 但是对于 de-CH , 我们就很难按字面推测说这是德语并且CH表示瑞士的拉丁语官方写法 "Confoederatio Helvetica" 的缩写.
在英文语言描述中, 语言名称在首, 其次是附加的属性标签, 使用逗号分隔并用圆括号结束, 例如:
语言标签 | iso.txt中的英文描述 |
---|---|
en | British English |
en-US | English (United States) |
en-053 | English (Australia & New Zealand) |
de | German |
de-CH-1996 | German (Switzerland, 1996 orthography) |
gws-1996 | Swiss German (1996 orthography) |
zh-cmn-Hans-CN | Mandarin Chinese (Simplified, Mainland China) |
zh-yue-Hant-HK | Cantonese Chinese (Traditional, Hong Kong) |
而对于本地语言描述, 仅需要按英文的描述翻译.
Unicode 的文字显示方向:
因为phpBB使用了UTF-8编码, 每个翻译人员都应该在翻译中考虑到文本的双向显示以免造成歧义.
双向文本中不同的Unicode控制字符和相应的HTML转义表示:
Unicode缩写 | Unicode code-point | Unicode 字符名称 | 相应的 HTML 标签 | 字符 (enclosed between ) |
---|---|---|---|---|
LRM | U+200E | Left-to-Right Mark | ‎ | '' |
RLM | U+200F | Right-to-Left Mark | ‏ | '' |
LRE | U+202A | Left-to-Right Embedding | dir="ltr" | '' |
RLE | U+202B | Right-to-Left Embedding | dir="rtl" | '' |
U+202C | Pop Directional Formatting | </bdo> | '' | |
LRO | U+202D | Left-to-Right Override | <bdo dir="ltr"> | '' |
RLO | U+202E | Right-to-Left Override | <bdo dir="rtl"> | '' |
对于iso.txt, 文本的方向可以明确的通过left-to-right/right-to-left markers/embeds/overrides这三种方式使用特殊的Unicode字符表示, 否则字符的显示顺序就会出错, 例如:
方向指示 | 语言说明 | 在 iso.txt 中的本地化显示说明 | 方向 |
---|---|---|---|
dir="ltr" | English (Australia & New Zealand) | English (Australia & New Zealand) | Correct |
dir="rtl" | English (Australia & New Zealand) | English (Australia & New Zealand) | Incorrect |
dir="rtl" with LRM | English (Australia & New Zealand)U+200E | English (Australia & New Zealand) | Correct |
dir="rtl" with LRE & PDF | U+202AEnglish (Australia & New Zealand)U+202C | English (Australia & New Zealand) | Correct |
dir="rtl" with LRO & PDF | U+202DEnglish (Australia & New Zealand)U+202C | English (Australia & New Zealand) | Correct |
在选择用何种方式时, 在大多数情况下, 使用LRM或RLM强制统一的方向就足够了. 而在某些情况下会出现从左到右与从右到左混杂的情况, 这时候使用 LRE & RLE with PDF会更合适. 最后, 在非常少的情况下需要强制方向时使用 LRO & RLO with PDF.
如果希望了解更多的双向文本显示方面的资料, 可以查看 W3C 关于XHTML页面显示双向文本的技术说明.
使用占位符:
因为phpBB会被翻译为不同方向的文本, 可能需要按不同的顺序显示各种数值. 例如一个简单的例子 "Page X of Y", 在英文中翻译为:
... 'PAGE_OF' => 'Page %s of %s', /* Just grabbing the replacements as they come and hope they are in the right order */ ...
… 一个更好的方法是明确的说明变量的顺序:
... 'PAGE_OF' => 'Page %1$s of %2$s', /* Explicit ordering of the replacements, even if they are the same order as English */ ...
这么做的原因是对于某些语言, 例如中文将会是这样:
... 'PAGE_OF' => '总计 %2$s 页, 当前在第 %1$s 页', /* Explicit ordering of the replacements, reversed compared to English as the total comes first */ ...
书写风格
小贴士
因为语言包由PHP文件组成, 文件中的字符串变量将在显示HTML页面的时候被引用, 所以需要遵守PHP和HTML的语法. 常见的易出错符号有: ' (单引号), " (双引号), < (小于号), > (大于号) 和 & (小鸟...-_-!).
// 错误写法 - 未转义的单引号将导致PHP语法解析错误
... 'CONV_ERROR_NO_AVATAR_PATH' => 'Note to developer: you must specify $convertor['avatar_path'] to use %s.', ...
// 正确写法 - 文本中的单引号需要使用反斜杠(/)转义
... 'CONV_ERROR_NO_AVATAR_PATH' => 'Note to developer: you must specify $convertor[/'avatar_path/'] to use %s.', ...
无论如何, 因为phpBB3 使用UTF-8 作为唯一编码, 我们可以利用UTF-8编码带来的好处, 某些时候并不需要使用单引号:
// 错误写法 - 未转义的单引号将导致PHP语法解析错误
... 'USE_PERMISSIONS' => 'Test out user's permissions', ...
// 正确写法 - 不过任何程序员都得小心别忘了这个转义符号
... 'USE_PERMISSIONS' => 'Test out user/'s permissions', ...
// 最好的写法 - 使用Unicode的单引符号
... 'USE_PERMISSIONS' => 'Test out user’s permissions', ...
" (双引号), < (小于号) and > (大于号) 可以被使用于HTML标签, 例如:
// 错误写法 - 无效的HTML语法, 因为存在错误未结束的标签
... 'FOO_BAR' => 'PHP version < 4.3.3.
Visit "Downloads" at <a href="http://www.php.net/">www.php.net</a>.', ...
// 正确写法 - HTML语法正确, 但是 """ 显得很很臃肿
... 'FOO_BAR' => 'PHP version < 4.3.3.
Visit "Downloads" at <a href="http://www.php.net/">www.php.net</a>.', ...
// 最好的写法 - HTML语法正确, 并且容易阅读
... 'FOO_BAR' => 'PHP version < 4.3.3.
Visit “Downloads” at <a href="http://www.php.net/">www.php.net</a>.', ...
最后, &符号必须被转义:
// 错误写法 - 无效的HTML语法
... 'FOO_BAR' => '<a href="http://somedomain.tld/?foo=1&bar=2">Foo & Bar</a>.', ...
// 正确写法 - HTML语法正确
... 'FOO_BAR' => '<a href="http://somedomain.tld/?foo=1&bar=2">Foo & Bar</a>.', ...
而这些字符的输入与操作系统, 使用的语种, 键盘设置和本地化特性的关系比较大. 请访问 http://en.wikipedia.org/wiki/Unicode#Input_methods 查看更多信息.
拼写, 标点, 语法等等:
phpBB使用的默认语言是British English, 使用Cambridge University Press标准拼写. 语言风格和语调尽量接近官方标准语言, 在分歧较大时采用适用人群最大的语法. 非正式的语法和一些俚语例如via需要尽量避免.