initial commit from hg

This commit is contained in:
Damien Elmes 2008-09-27 23:50:03 +09:00
commit 5da3a0f5d3
64 changed files with 18653 additions and 0 deletions

9
.gitignore vendored Normal file
View file

@ -0,0 +1,9 @@
*.pyc
*~
*.mo
*\#
build
dist
anki.egg-info
samples
unihan.db

676
COPYING Normal file
View file

@ -0,0 +1,676 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<http://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<http://www.gnu.org/philosophy/why-not-lgpl.html>.

5091
ChangeLog.old Normal file

File diff suppressed because it is too large Load diff

58
anki/__init__.py Normal file
View file

@ -0,0 +1,58 @@
# -*- coding: utf-8 -*-
# Copyright: Damien Elmes <anki@ichi2.net>
# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html
"""\
Anki (libanki)
====================
Open a deck:
deck = anki.DeckStorage.Deck(path)
Get a card:
card = deck.getCard()
if not card:
# deck is finished
Show the card:
print card.question, card.answer
Answer the card:
deck.answerCard(card, ease)
Edit the card:
fields = card.fact.model.fieldModels
for field in fields:
card.fact[field.name] = "newvalue"
card.fact.setModified(textChanged=True)
deck.setModified()
Get all cards via ORM (slow):
from anki.cards import Card
cards = deck.s.query(Card).all()
Get all q/a/ids via SQL (fast):
cards = deck.s.all("select id, question, answer from cards")
Save & close:
deck.save()
deck.close()
"""
__docformat__ = 'restructuredtext'
try:
__import__('pkg_resources').declare_namespace(__name__)
except ImportError:
pass
version = "0.9.8.1"
from anki.deck import DeckStorage

250
anki/cards.py Normal file
View file

@ -0,0 +1,250 @@
# -*- coding: utf-8 -*-
# Copyright: Damien Elmes <anki@ichi2.net>
# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html
"""\
Cards
====================
"""
__docformat__ = 'restructuredtext'
import time, sys, math, random
from anki.db import *
from anki.models import CardModel, Model, FieldModel
from anki.facts import Fact, factsTable, Field
from anki.utils import parseTags, findTag, stripHTML, genID
# Cards
##########################################################################
cardsTable = Table(
'cards', metadata,
Column('id', Integer, primary_key=True),
Column('factId', Integer, ForeignKey("facts.id"), nullable=False),
Column('cardModelId', Integer, ForeignKey("cardModels.id"), nullable=False),
Column('created', Float, nullable=False, default=time.time),
Column('modified', Float, nullable=False, default=time.time),
Column('tags', UnicodeText, nullable=False, default=u""),
Column('ordinal', Integer, nullable=False),
# cached - changed on fact update
Column('question', UnicodeText, nullable=False, default=u""),
Column('answer', UnicodeText, nullable=False, default=u""),
# default to 'normal' priority;
# this is indexed in deck.py as we need to create a reverse index
Column('priority', Integer, nullable=False, default=2),
Column('interval', Float, nullable=False, default=0),
Column('lastInterval', Float, nullable=False, default=0),
Column('due', Float, nullable=False, default=time.time),
Column('lastDue', Float, nullable=False, default=0),
Column('factor', Float, nullable=False, default=2.5),
Column('lastFactor', Float, nullable=False, default=2.5),
Column('firstAnswered', Float, nullable=False, default=0),
# stats
Column('reps', Integer, nullable=False, default=0),
Column('successive', Integer, nullable=False, default=0),
Column('averageTime', Float, nullable=False, default=0),
Column('reviewTime', Float, nullable=False, default=0),
Column('youngEase0', Integer, nullable=False, default=0),
Column('youngEase1', Integer, nullable=False, default=0),
Column('youngEase2', Integer, nullable=False, default=0),
Column('youngEase3', Integer, nullable=False, default=0),
Column('youngEase4', Integer, nullable=False, default=0),
Column('matureEase0', Integer, nullable=False, default=0),
Column('matureEase1', Integer, nullable=False, default=0),
Column('matureEase2', Integer, nullable=False, default=0),
Column('matureEase3', Integer, nullable=False, default=0),
Column('matureEase4', Integer, nullable=False, default=0),
# this duplicates the above data, because there's no way to map imported
# data to the above
Column('yesCount', Integer, nullable=False, default=0),
Column('noCount', Integer, nullable=False, default=0),
# cache
Column('spaceUntil', Float, nullable=False, default=0),
Column('relativeDelay', Float, nullable=False, default=0),
Column('isDue', Boolean, nullable=False, default=0),
Column('type', Integer, nullable=False, default=2),
Column('combinedDue', Integer, nullable=False, default=0))
class Card(object):
"A card."
def __init__(self, fact=None, cardModel=None):
self.tags = u""
self.id = genID()
# new cards start as new & due
self.type = 2
self.isDue = True
self.timerStarted = False
self.timerStopped = False
if fact:
self.fact = fact
if cardModel:
self.cardModel = cardModel
self.ordinal = cardModel.ordinal
self.question = cardModel.renderQA(self, self.fact, "question")
self.answer = cardModel.renderQA(self, self.fact, "answer")
htmlQuestion = property(lambda self: self.cardModel.renderQA(
self, self.fact, "question", format="html"))
htmlAnswer = property(lambda self: self.cardModel.renderQA(
self, self.fact, "answer", format="html"))
def setModified(self):
self.modified = time.time()
def startTimer(self):
self.timerStarted = time.time()
def stopTimer(self):
self.timerStopped = time.time()
def thinkingTime(self):
"Return the time this card's been shown."
return (self.timerStopped or time.time()) - self.timerStarted
def css(self):
return self.cardModel.css() + self.fact.css()
def genFuzz(self):
"Generate a random offset to spread intervals."
self.fuzz = random.uniform(0.95, 1.05)
def updateStats(self, ease, state):
self.reps += 1
if ease > 1:
self.successive += 1
else:
self.successive = 0
delay = self.thinkingTime()
# ignore any times over 60 seconds
if delay < 60:
self.reviewTime += delay
if self.averageTime:
self.averageTime = (self.averageTime + delay) / 2.0
else:
self.averageTime = delay
# we don't track first answer for cards
if state == "new":
state = "young"
# update ease and yes/no count
attr = state + "Ease%d" % ease
setattr(self, attr, getattr(self, attr) + 1)
if ease < 2:
self.noCount += 1
else:
self.yesCount += 1
if not self.firstAnswered:
self.firstAnswered = time.time()
self.setModified()
def hasTag(self, tag):
alltags = parseTags(self.tags + "," +
self.fact.tags + "," +
self.cardModel.name + "," +
self.fact.model.tags)
return findTag(tag, alltags)
def fromDB(self, s, id):
r = s.first("select * from cards where id = :id",
id=id)
(self.id,
self.factId,
self.cardModelId,
self.created,
self.modified,
self.tags,
self.ordinal,
self.question,
self.answer,
self.priority,
self.interval,
self.lastInterval,
self.due,
self.lastDue,
self.factor,
self.lastFactor,
self.firstAnswered,
self.reps,
self.successive,
self.averageTime,
self.reviewTime,
self.youngEase0,
self.youngEase1,
self.youngEase2,
self.youngEase3,
self.youngEase4,
self.matureEase0,
self.matureEase1,
self.matureEase2,
self.matureEase3,
self.matureEase4,
self.yesCount,
self.noCount,
self.spaceUntil,
self.relativeDelay,
self.isDue,
self.type,
self.combinedDue) = r
def toDB(self, s):
"Write card to DB. Note that isDue assumes card is not spaced."
if self.reps == 0:
self.type = 2
elif self.successive:
self.type = 1
else:
self.type = 0
s.execute("""update cards set
modified=:modified,
tags=:tags,
interval=:interval,
lastInterval=:lastInterval,
due=:due,
lastDue=:lastDue,
factor=:factor,
lastFactor=:lastFactor,
firstAnswered=:firstAnswered,
reps=:reps,
successive=:successive,
averageTime=:averageTime,
reviewTime=:reviewTime,
youngEase0=:youngEase0,
youngEase1=:youngEase1,
youngEase2=:youngEase2,
youngEase3=:youngEase3,
youngEase4=:youngEase4,
matureEase0=:matureEase0,
matureEase1=:matureEase1,
matureEase2=:matureEase2,
matureEase3=:matureEase3,
matureEase4=:matureEase4,
yesCount=:yesCount,
noCount=:noCount,
spaceUntil = :spaceUntil,
relativeDelay = :interval / (strftime("%s", "now") - :due + 1),
isDue = :isDue,
type = :type,
combinedDue = max(:spaceUntil, :due)
where id=:id""", self.__dict__)
mapper(Card, cardsTable, properties={
'cardModel': relation(CardModel),
'fact': relation(Fact, backref="cards", primaryjoin=
cardsTable.c.factId == factsTable.c.id),
})
mapper(Fact, factsTable, properties={
'model': relation(Model),
'fields': relation(Field, backref="fact", order_by=Field.c.ordinal),
'lastCard': relation(Card, post_update=True, primaryjoin=
cardsTable.c.id == factsTable.c.lastCardId),
})
# Card deletions
##########################################################################
cardsDeletedTable = Table(
'cardsDeleted', metadata,
Column('cardId', Integer, ForeignKey("cards.id"),
nullable=False),
Column('deletedTime', Float, nullable=False))

100
anki/db.py Normal file
View file

@ -0,0 +1,100 @@
# -*- coding: utf-8 -*-
# Copyright: Damien Elmes <anki@ichi2.net>
# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html
"""\
DB tools
====================
SessionHelper is a wrapper for the standard sqlalchemy session, which provides
some convenience routines, and manages transactions itself.
object_session() is a replacement for the standard object_session(), which
provides the features of SessionHelper, and avoids taking out another
transaction.
"""
__docformat__ = 'restructuredtext'
try:
from pysqlite2 import dbapi2 as sqlite
except ImportError:
try:
from sqlite3 import dbapi2 as sqlite
except:
raise "Please install pysqlite2 or python2.5"
sqlite.enable_shared_cache(True)
from sqlalchemy import (Table, Integer, Float, Column, MetaData,
ForeignKey, Boolean, String, Date, UniqueConstraint)
from sqlalchemy import create_engine
from sqlalchemy.orm import mapper, sessionmaker, relation, backref, \
object_session as _object_session
from sqlalchemy.sql import select, text, and_
from sqlalchemy.exceptions import DBAPIError, OperationalError
# sqlalchemy didn't handle the move to unicodetext nicely
try:
from sqlalchemy import UnicodeText
except ImportError:
from sqlalchemy import Unicode
UnicodeText = Unicode
metadata = MetaData()
# this class assumes the provided session is called with transactional=False
class SessionHelper(object):
"Add some convenience routines to a session."
def __init__(self, session, lock=False, transaction=True):
self._session = session
self._lock = lock
self._transaction = transaction
if self._transaction:
self._session.begin()
if self._lock:
self._lockDB()
self._seen = True
def __getattr__(self, k):
return getattr(self.__dict__['_session'], k)
def scalar(self, sql, **args):
return self.execute(text(sql), args).scalar()
def all(self, sql, **args):
return self.execute(text(sql), args).fetchall()
def first(self, sql, **args):
return self.execute(text(sql), args).fetchone()
def column0(self, sql, **args):
return [x[0] for x in self.execute(text(sql), args).fetchall()]
def statement(self, sql, **kwargs):
"Execute a statement without returning any results. Flush first."
self.execute(text(sql), kwargs)
def statements(self, sql, data):
"Execute a statement across data. Flush first."
self.execute(text(sql), data)
def __repr__(self):
return repr(self._session)
def commit(self):
self._session.commit()
if self._transaction:
self._session.begin()
if self._lock:
self._lockDB()
def _lockDB(self):
"Take out a write lock."
self._session.execute(text("update decks set modified=modified"))
def object_session(*args):
s = _object_session(*args)
if s:
return SessionHelper(s, transaction=False)
return None

1825
anki/deck.py Normal file

File diff suppressed because it is too large Load diff

58
anki/errors.py Normal file
View file

@ -0,0 +1,58 @@
# -*- coding: utf-8 -*-
# Copyright: Damien Elmes <anki@ichi2.net>
# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html
"""\
Errors
==============================
"""
__docformat__ = 'restructuredtext'
class Error(Exception):
def __init__(self, message="", **data):
self.data = data
self.message = message
def __str__(self):
m = self.message
if self.data:
m += ": %s" % repr(self.data)
return m
class DeckAccessError(Error):
"The deck is empty."
pass
class DeckWrongFormatError(Error):
"A file to import is in the wrong format."
pass
class DuplicateCardError(Error):
"Attempted to add a card with the same question."
pass
class ImportFileError(Error):
"Unable to load file to import from."
pass
class ImportFormatError(Error):
"Unable to determine pattern in text file."
pass
class ImportEncodingError(Error):
"The file was not in utf-8."
pass
class ExportFileError(Error):
"Unable to save file."
pass
class SyncError(Error):
"A problem occurred during syncing."
pass
# facts, models
class FactInvalidError(Error):
"""A fact was invalid/not unique according to the model.
'field' defines the problem field.
'type' defines the type of error ('fieldEmpty', 'fieldNotUnique')"""
pass

218
anki/exporting.py Normal file
View file

@ -0,0 +1,218 @@
# -*- coding: utf-8 -*-
# Copyright: Damien Elmes <anki@ichi2.net>
# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html
"""\
Exporting support
==============================
"""
__docformat__ = 'restructuredtext'
import itertools, time
from operator import itemgetter
from anki import DeckStorage
from anki.cards import Card
from anki.sync import SyncClient, SyncServer
from anki.lang import _
from anki.utils import findTag, parseTags, stripHTML, ids2str
from anki.db import *
class Exporter(object):
def __init__(self, deck):
self.deck = deck
self.limitTags = []
def exportInto(self, path):
file = open(path, "wb")
self.doExport(file)
file.close()
def escapeText(self, text):
"Escape newlines and tabs."
text = text.replace("\n", "<br>")
text = text.replace("\t", " " * 8)
return text
def cardIds(self):
"Return all cards, limited by tags."
tlist = self.deck.tagsList()
cards = [id for (id, tags, pri) in tlist if self.hasTags(tags)]
self.count = len(cards)
return cards
def hasTags(self, tags):
tags = parseTags(tags)
if not self.limitTags:
return True
for tag in self.limitTags:
if findTag(tag, tags):
return True
return False
# FIXME: media support
class AnkiExporter(Exporter):
key = _("Anki decks (*.anki)")
ext = ".anki"
def __init__(self, deck):
Exporter.__init__(self, deck)
self.includeSchedulingInfo = False
def exportInto(self, path):
self.newDeck = DeckStorage.Deck(path)
client = SyncClient(self.deck)
server = SyncServer(self.newDeck)
client.setServer(server)
client.localTime = self.deck.modified
client.remoteTime = 0
self.deck.s.flush()
# set up a custom change list and sync
lsum = self.localSummary()
rsum = server.summary(0)
payload = client.genPayload((lsum, rsum))
res = server.applyPayload(payload)
client.applyPayloadReply(res)
if not self.includeSchedulingInfo:
self.newDeck.s.statement("""
update cards set
interval = 0.001,
lastInterval = 0,
due = created,
lastDue = 0,
factor = 2.5,
firstAnswered = 0,
reps = 0,
successive = 0,
averageTime = 0,
reviewTime = 0,
youngEase0 = 0,
youngEase1 = 0,
youngEase2 = 0,
youngEase3 = 0,
youngEase4 = 0,
matureEase0 = 0,
matureEase1 = 0,
matureEase2 = 0,
matureEase3 = 0,
matureEase4 = 0,
yesCount = 0,
noCount = 0,
spaceUntil = 0,
isDue = 1,
relativeDelay = 0,
type = 2,
combinedDue = created,
modified = :now
""", now=time.time())
# need to save manually
self.newDeck.s.commit()
self.newDeck.close()
def localSummary(self):
cardIds = self.cardIds()
cStrIds = ids2str(cardIds)
cards = self.deck.s.all("""
select id, modified from cards
where id in %s""" % cStrIds)
facts = self.deck.s.all("""
select facts.id, facts.modified from cards, facts where
facts.id = cards.factId and
cards.id in %s""" % cStrIds)
models = self.deck.s.all("""
select models.id, models.modified from models, facts where
facts.modelId = models.id and
facts.id in %s""" % ids2str([f[0] for f in facts]))
media = self.deck.s.all("""
select id, created from media""")
return {
# cards
"cards": cards,
"delcards": [],
# facts
"facts": facts,
"delfacts": [],
# models
"models": models,
"delmodels": [],
# media
"media": media,
"delmedia": [],
}
class TextCardExporter(Exporter):
key = _("Text files (*.txt)")
ext = ".txt"
def __init__(self, deck):
Exporter.__init__(self, deck)
self.includeTags = False
def doExport(self, file):
strids = ids2str(self.cardIds())
cards = self.deck.s.all("""
select cards.question, cards.answer, cards.id from cards
where cards.id in %s""" % strids)
if self.includeTags:
self.cardTags = dict(self.deck.s.all("""
select cards.id, cards.tags || "," || facts.tags from cards, facts
where cards.factId = facts.id
and cards.id in %s""" % strids))
out = u"\n".join(["%s\t%s%s" % (self.escapeText(c[0]),
self.escapeText(c[1]),
self.tags(c[2]))
for c in cards])
if out:
out += "\n"
file.write(out.encode("utf-8"))
def tags(self, id):
if self.includeTags:
return "\t" + ", ".join(parseTags(self.cardTags[id]))
return ""
class TextFactExporter(Exporter):
key = _("Text files (*.txt)")
ext = ".txt"
def __init__(self, deck):
Exporter.__init__(self, deck)
self.includeTags = False
def doExport(self, file):
cardIds = self.cardIds()
facts = self.deck.s.all("""
select factId, value from fields
where
factId in
(select distinct facts.id from facts, cards
where facts.id = cards.factId
and cards.id in %s)
order by factId, ordinal""" % ids2str(cardIds))
txt = ""
if self.includeTags:
self.factTags = dict(self.deck.s.all(
"select id, tags from facts where id in %s" %
ids2str([fact[0] for fact in facts])))
out = ["\t".join([self.escapeText(x[1]) for x in ret[1]]) +
self.tags(ret[0])
for ret in (itertools.groupby(facts, itemgetter(0)))]
self.count = len(out)
out = "\n".join(out)
file.write(out.encode("utf-8"))
def tags(self, id):
if self.includeTags:
return "\t" + self.factTags[id]
return ""
# Export modules
##########################################################################
def exporters():
return (
(_("Anki deck (*.anki)"), AnkiExporter),
(_("Cards in tab-separated text file (*.txt)"), TextCardExporter),
(_("Facts in tab-separated text file (*.txt)"), TextFactExporter))

150
anki/facts.py Normal file
View file

@ -0,0 +1,150 @@
# -*- coding: utf-8 -*-
# Copyright: Damien Elmes <anki@ichi2.net>
# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html
"""\
Facts
========
"""
__docformat__ = 'restructuredtext'
import time
from anki.db import *
from anki.errors import *
from anki.models import Model, FieldModel, fieldModelsTable
from anki.utils import genID
from anki.features import FeatureManager
# Fields in a fact
##########################################################################
fieldsTable = Table(
'fields', metadata,
Column('id', Integer, primary_key=True),
Column('factId', Integer, ForeignKey("facts.id"), nullable=False),
Column('fieldModelId', Integer, ForeignKey("fieldModels.id"),
nullable=False),
Column('ordinal', Integer, nullable=False),
Column('value', UnicodeText, nullable=False))
class Field(object):
"A field in a fact."
def __init__(self, fieldModel=None):
if fieldModel:
self.fieldModel = fieldModel
self.ordinal = fieldModel.ordinal
self.value = u""
self.id = genID()
def getName(self):
return self.fieldModel.name
name = property(getName)
mapper(Field, fieldsTable, properties={
'fieldModel': relation(FieldModel)
})
# Facts: a set of fields and a model
##########################################################################
# mapped in cards.py
factsTable = Table(
'facts', metadata,
Column('id', Integer, primary_key=True),
Column('modelId', Integer, ForeignKey("models.id"), nullable=False),
Column('created', Float, nullable=False, default=time.time),
Column('modified', Float, nullable=False, default=time.time),
Column('tags', UnicodeText, nullable=False, default=u""),
# the following two fields are obsolete and now stored in cards table
Column('spaceUntil', Float, nullable=False, default=0),
Column('lastCardId', Integer, ForeignKey(
"cards.id", use_alter=True, name="lastCardIdfk")))
class Fact(object):
"A single fact. Fields exposed as dict interface."
def __init__(self, model=None):
self.model = model
self.id = genID()
if model:
for fm in model.fieldModels:
self.fields.append(Field(fm))
def keys(self):
return [field.name for field in self.fields]
def values(self):
return [field.value for field in self.fields]
def __getitem__(self, key):
try:
return [f.value for f in self.fields if f.name == key][0]
except IndexError:
raise KeyError
def __setitem__(self, key, value):
try:
[f for f in self.fields if f.name == key][0].value = value
except IndexError:
raise KeyError
def get(self, key, default):
try:
return self[key]
except IndexError:
return default
def css(self):
return "".join([f.fieldModel.css() for f in self.fields])
def assertValid(self):
"Raise an error if required fields are empty."
for field in self.fields:
if not self.fieldValid(field):
raise FactInvalidError(type="fieldEmpty",
field=field.name)
def fieldValid(self, field):
return not (field.fieldModel.required and not field.value.strip())
def assertUnique(self, s):
"Raise an error if duplicate fields are found."
for field in self.fields:
if not self.fieldUnique(field, s):
raise FactInvalidError(type="fieldNotUnique",
field=field.name)
def fieldUnique(self, field, s):
if not field.fieldModel.unique:
return True
req = ("select value from fields "
"where fieldModelId = :fmid and value = :val")
if field.id:
req += " and id != %s" % field.id
return not s.scalar(req, val=field.value, fmid=field.fieldModel.id)
def onSubmit(self):
FeatureManager.run(self.model.features, "onSubmit", self)
def onKeyPress(self, field, value):
FeatureManager.run(self.model.features,
"onKeyPress", self, field, value)
def setModified(self, textChanged=False):
"Mark modified and update cards."
self.modified = time.time()
if textChanged:
for card in self.cards:
card.question = card.cardModel.renderQA(card, self, "question")
card.answer = card.cardModel.renderQA(card, self, "answer")
card.setModified()
# Fact deletions
##########################################################################
factsDeletedTable = Table(
'factsDeleted', metadata,
Column('factId', Integer, ForeignKey("facts.id"),
nullable=False),
Column('deletedTime', Float, nullable=False))

65
anki/features/__init__.py Normal file
View file

@ -0,0 +1,65 @@
# -*- coding: utf-8 -*-
# Copyright: Damien Elmes <anki@ichi2.net>
# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html
"""\
Features - extensible features like auto-reading generation
===============================================================================
Features allow the deck to define specific features that are required, but
that can be resolved in real time. This includes things like automatic reading
generation, language-specific dictionary entries, etc.
"""
from anki.lang import _
from anki.errors import *
from anki.utils import findTag, parseTags
class Feature(object):
def __init__(self, tags=None, name="", description=""):
if not tags:
tags = []
self.tags = tags
self.name = name
self.description = description
def onSubmit(self, fact):
"Apply any last-minute modifications to FACT before addition."
pass
def onKeyPress(self, fact):
"Apply any changes to fact as it's being edited for the first time."
pass
def run(self, cmd, *args):
"Run CMD."
attr = getattr(self, cmd, None)
if attr:
attr(*args)
class FeatureManager(object):
features = {}
def add(feature):
"Add a feature."
FeatureManager.features[feature.name] = feature
add = staticmethod(add)
def run(tagstr, cmd, *args):
"Run CMD on all matching features in DLIST."
tags = parseTags(tagstr)
for (name, feature) in FeatureManager.features.items():
for tag in tags:
if findTag(tag, feature.tags):
feature.run(cmd, *args)
break
run = staticmethod(run)
# Add bundled features
import japanese
FeatureManager.add(japanese.FuriganaGenerator())
import chinese
FeatureManager.add(chinese.CantoneseGenerator())
FeatureManager.add(chinese.MandarinGenerator())

View file

@ -0,0 +1,4 @@
Downloaded from http://www.unicode.org/Public/4.1.0/ucd/Unihan.zip
Copyright: http://www.unicode.org/copyright.html

View file

@ -0,0 +1,89 @@
# -*- coding: utf-8 -*-
# Copyright: Damien Elmes <anki@ichi2.net>
# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html
import sys, os, pickle
from anki.features import Feature
from anki.utils import findTag, parseTags, stripHTML
from anki.db import *
class UnihanController(object):
def __init__(self, target):
base = os.path.dirname(os.path.abspath(__file__))
if not os.path.exists(base):
if sys.platform.startswith("darwin"):
base = os.path.dirname(sys.argv[0])
else:
base = os.path.join(os.path.dirname(sys.argv[0]),
"features")
self.engine = create_engine("sqlite:///" + os.path.abspath(
os.path.join(base, "unihan.db")),
echo=False, strategy='threadlocal')
self.session = sessionmaker(bind=self.engine,
autoflush=False,
transactional=True)
self.type = target
def reading(self, text):
text = stripHTML(text)
result = []
s = SessionHelper(self.session())
for c in text:
n = ord(c)
ret = s.scalar("select %s from unihan where id = :id"
% self.type, id=n)
if ret:
result.append(self.formatMatch(ret))
return u" ".join(result)
def formatMatch(self, match):
m = match.split(" ")
if len(m) == 1:
return m[0]
return "{%s}" % (",".join(m))
class ChineseGenerator(Feature):
def __init__(self):
self.expressionField = "Expression"
self.readingField = "Reading"
def lazyInit(self):
pass
def onKeyPress(self, fact, field, value):
if findTag("Reading source", parseTags(field.fieldModel.features)):
dst = None
for field in fact.fields:
if findTag("Reading destination",
parseTags(field.fieldModel.features)):
dst = field
break
if not dst:
return
self.lazyInit()
reading = self.unihan.reading(value)
fact[dst.name] = reading
class CantoneseGenerator(ChineseGenerator):
def __init__(self):
ChineseGenerator.__init__(self)
self.tags = ["Cantonese"]
self.name = "Reading generation for Cantonese"
def lazyInit(self):
if 'unihan' not in self.__dict__:
self.unihan = UnihanController("cantonese")
class MandarinGenerator(ChineseGenerator):
def __init__(self):
ChineseGenerator.__init__(self)
self.tags = ["Mandarin"]
self.name = "Reading generation for Mandarin"
def lazyInit(self):
if 'unihan' not in self.__dict__:
self.unihan = UnihanController("mandarin")

View file

@ -0,0 +1,101 @@
# -*- coding: utf-8 -*-
# read unihan.txt and save it as a db
import psyco; psyco.full()
from sqlalchemy import (Table, Integer, Float, Unicode, Column, MetaData,
ForeignKey, Boolean, String, Date, UniqueConstraint,
UnicodeText)
from sqlalchemy import (create_engine)
from sqlalchemy.orm import mapper, sessionmaker, relation, backref, \
object_session as _object_session
from sqlalchemy.sql import select, text, and_
from sqlalchemy.exceptions import DBAPIError
import re
metadata = MetaData()
unihanTable = Table(
'unihan', metadata,
Column("id", Integer, primary_key=True),
Column("mandarin", UnicodeText),
Column("cantonese", UnicodeText),
Column("grade", Integer),
)
engine = create_engine("sqlite:///unihan.db",
echo=False, strategy='threadlocal')
session = sessionmaker(bind=engine,
autoflush=False,
transactional=True)
metadata.create_all(engine)
s = session()
# Convert codes to accents
##########################################################################
# code from Donald Chai
accenttable = {
'a' : [u'a', u'ā', u'á', u'ǎ', u'à', u'a'],
'e' : [u'e', u'ē', u'é', u'ě', u'è', u'e'],
'i' : [u'i', u'ī', u'í', u'ǐ', u'ì', u'i'],
'o' : [u'o', u'ō', u'ó', u'ǒ', u'ò', u'o'],
'u' : [u'u', u'ū', u'ú', u'ǔ', u'ù', u'u'],
'v' : [u'ü', u'ǖ', u'ǘ', u'ǚ', u'ǜ', u'ü'],
}
def convert(word):
'''Converts a pinyin word to unicode'''
word = word.lower()
orig = word
# convert ü to v for now to make life easier
word = re.sub(u'\xfc|\xc3\xbc', 'v', word)
# extract fields
mo = re.match('([qwrtypsdfghjklzxcbnm]*)([aeiouv]*)(\D*)(\d?)', word)
init = mo.group(1)
vowel = mo.group(2)
final = mo.group(3)
tone = mo.group(4)
# do nothing if no vowel or tone
if vowel=='' or tone=='':
return orig
tone = int(tone)
if len(vowel)==1:
vowel = accenttable[vowel][tone]
elif vowel[-2]=='i' or vowel[-2]=='u':
# put over last
vowel = vowel[:-1] + accenttable[vowel[-1]][tone]
else:
# put over second to last
vowel = vowel[:-2] + accenttable[vowel[-2]][tone] + vowel[-1]
return init+vowel+final
##########################################################################
kanji = {}
import codecs
for line in codecs.open("Unihan.txt", encoding="utf-8"):
try:
(u, f, v) = line.strip().split("\t")
except ValueError:
continue
if not u.startswith("U+"):
continue
n = int(u.replace("U+",""), 16)
if not n in kanji:
kanji[n] = {}
if f == "kMandarin":
kanji[n]['mandarin'] = " ".join([convert(w) for w in v.split()])
elif f == "kCantonese":
kanji[n]['cantonese'] = v
elif f == "kGradeLevel":
kanji[n]['grade'] = int(v)
dict = [{'id': k,
'mandarin': v.get('mandarin'),
'cantonese': v.get('cantonese'),
'grade': v.get('grade') } for (k,v) in kanji.items()
if v.get('mandarin') or v.get('cantonese') or v.get('grade')]
s.execute(unihanTable.insert(), dict)
s.commit()

106
anki/features/japanese.py Normal file
View file

@ -0,0 +1,106 @@
# -*- coding: utf-8 -*-
# Copyright: Damien Elmes <anki@ichi2.net>
# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html
import sys, os
from anki.features import Feature
from anki.utils import findTag, parseTags, stripHTML
class KakasiController(object):
def __init__(self):
# add our pre-packaged kakasi to the path
if sys.platform == "win32":
dir = os.path.dirname(sys.argv[0])
os.environ['PATH'] += ";" + dir + "\\kakasi\\bin"
shareDir = dir + "\\kakasi\\share\\kakasi\\"
os.environ['ITAIJIDICT'] = shareDir + "\\itaijidict"
os.environ['KANWADICT'] = shareDir + "\\kanwadict"
elif sys.platform.startswith("darwin"):
dir = os.path.dirname(os.path.abspath(__file__))
dir = os.path.abspath(dir + "/../../../../..")
import platform
# don't add kakasi to the path on powerpc, it's buggy
# and loop until this works, since processor() is buggy
while 1:
try:
proc = platform.processor()
except IOError:
proc = None
if proc:
break
if proc != "powerpc":
os.environ['PATH'] += ":" + dir + "/kakasi"
os.environ['ITAIJIDICT'] = dir + "/kakasi/itaijidict"
os.environ['KANWADICT'] = dir + "/kakasi/kanwadict"
self._open = False
# we don't launch kakasi until it's actually required
def lazyopen(self):
if not self._open:
if not self.available():
from errno import ENOENT
raise OSError(ENOENT, 'Kakasi not available')
# don't convert kana to hiragana
(self.kin, self.kout) = os.popen2("kakasi -isjis -osjis -u -JH -p")
self._open = True
def close(self):
if self._open:
self.kin.close()
self.kout.close()
def toFurigana(self, kanji):
self.lazyopen()
kanji = self.formatForKakasi(kanji)
try:
self.kin.write(kanji.encode("sjis", "ignore")+'\n')
self.kin.flush()
return unicode(self.kout.readline().rstrip('\n'), "sjis")
except IOError:
return u""
def formatForKakasi(self, text):
"Strip characters kakasi can't handle."
# kakasi is line based
text = text.replace("\n", " ")
text = text.replace(u'\uff5e', "~")
text = text.replace("<br>", "---newline---")
text = text.replace("<br />", "---newline---")
text = stripHTML(text)
text = text.replace("---newline---", "<br>")
return text
def available(self):
if sys.platform in ("win32",):
executable = 'kakasi.exe'
else:
executable = 'kakasi'
for d in os.environ['PATH'].split(os.pathsep):
if os.path.exists(os.path.join(d, executable)):
return True
return False
class FuriganaGenerator(Feature):
def __init__(self):
self.tags = ["Japanese"]
self.name = "Furigana generation based on kakasi."
self.kakasi = KakasiController()
if not self.kakasi.available():
self.kakasi = None
def onKeyPress(self, fact, field, value):
if self.kakasi and findTag("Reading source",
parseTags(field.fieldModel.features)):
reading = self.kakasi.toFurigana(value)
dst = None
for field in fact.fields:
if findTag("Reading destination", parseTags(
field.fieldModel.features)):
dst = field
break
if dst:
if self.kakasi.formatForKakasi(value) != reading:
fact[dst.name] = reading
else:
fact[dst.name] = u""

55
anki/fonts.py Normal file
View file

@ -0,0 +1,55 @@
# -*- coding: utf-8 -*-
# Copyright: Damien Elmes <anki@ichi2.net>
# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html
"""\
Fonts - mapping to/from platform-specific fonts
==============================================================
"""
import sys
# set this to 'all', to get all fonts in a list
policy="platform"
mapping = [
[u"Mincho", u"MS Mincho", "win32"],
[u"Mincho", u" 明朝", "win32"],
[u"Mincho", u"ヒラギノ明朝 Pro W3", "mac"],
[u"Mincho", u"Kochi Mincho", "linux"],
[u"Mincho", u"東風明朝", "linux"],
]
def platform():
if sys.platform == "win32":
return "win32"
elif sys.platform.startswith("darwin"):
return "mac"
else:
return "linux"
def toCanonicalFont(family):
"Turn a platform-specific family into a canonical one."
for (s, p, type) in mapping:
if family == p:
return s
return family
def toPlatformFont(family):
"Turn a canonical font into a platform-specific one."
if policy == "all":
return allFonts(family)
ltype = platform()
for (s, p, type) in mapping:
if family == s and type == ltype:
return p
return family
def substitutions():
"Return a tuple mapping canonical fonts to platform ones."
type = platform()
return [(s, p) for (s, p, t) in mapping if t == type]
def allFonts(family):
ret = ", ".join([p for (s, p, t) in mapping if s == family])
return ret or family

216
anki/graphs.py Normal file
View file

@ -0,0 +1,216 @@
# -*- coding: utf-8 -*-
# Copyright: Damien Elmes <anki@ichi2.net>
# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html
"""\
Graphs of deck statistics
==============================
"""
__docformat__ = 'restructuredtext'
import os, sys, time
import anki.stats
# support frozen distribs
if getattr(sys, "frozen", None):
os.environ['MATPLOTLIBDATA'] = os.path.join(
os.path.dirname(sys.argv[0]),
"matplotlibdata")
try:
from matplotlib.figure import Figure
except UnicodeEncodeError:
# haven't tracked down the cause of this yet, but reloading fixes it
try:
from matplotlib.figure import Figure
except ImportError:
pass
except ImportError:
pass
def graphsAvailable():
return 'matplotlib' in sys.modules
class DeckGraphs(object):
def __init__(self, deck, width=8, height=3, dpi=75):
self.deck = deck
self.stats = None
self.width = width
self.height = height
self.dpi = dpi
def calcStats (self):
if not self.stats:
days = {}
months = {}
next = {}
lowestInDay = 0
now = list(time.gmtime(time.time()))
now[3] = 0; now[4] = 0
self.startOfDay = time.mktime(now)
all = self.deck.s.all("""
select interval, combinedDue
from cards where reps > 0 and priority != 0""")
for (interval, due) in all:
day=int(round(interval))
days[day] = days.get(day, 0) + 1
indays = int(round((due - self.startOfDay)
/ 86400.0))
next[indays] = next.get(indays, 0) + 1
if indays < lowestInDay:
lowestInDay = indays
self.stats = {}
self.stats['next'] = next
self.stats['days'] = days
self.stats['months'] = months
self.stats['lowestInDay'] = lowestInDay
def nextDue(self, days=30):
self.calcStats()
fig = Figure(figsize=(self.width, self.height), dpi=self.dpi)
graph = fig.add_subplot(111)
dayslist = self.stats['next']
self.addMissing(dayslist, self.stats['lowestInDay'], days)
(x, y) = self.unzip(dayslist.items())
self.filledGraph(graph, days, x, y, "#4444ff")
graph.set_ylabel(_("Cards"))
graph.set_xlabel(_("Days"))
graph.set_xlim(xmin=self.stats['lowestInDay'], xmax=days)
return fig
def cumulativeDue(self, days=30):
self.calcStats()
fig = Figure(figsize=(self.width, self.height), dpi=self.dpi)
graph = fig.add_subplot(111)
(x, y) = self.unzip(self.stats['next'].items())
count=0
y = list(y)
for i in range(len(x)):
count = count + y[i]
if i == 0:
continue
y[i] = count
if x[i] > days:
break
x = list(x); x.append(99999)
y.append(count)
self.filledGraph(graph, days, x, y, "#aaaaff")
graph.set_ylabel(_("Cards"))
graph.set_xlabel(_("Days"))
graph.set_xlim(xmin=self.stats['lowestInDay'], xmax=days)
graph.set_ylim(ymax=count+100)
return fig
def intervalPeriod(self, days=30):
self.calcStats()
fig = Figure(figsize=(self.width, self.height), dpi=self.dpi)
ints = self.stats['days']
self.addMissing(ints, 0, days)
intervals = self.unzip(ints.items())
graph = fig.add_subplot(111)
self.filledGraph(graph, days, intervals[0], intervals[1], "#aaffaa")
graph.set_ylabel(_("Cards"))
graph.set_xlabel(_("Days"))
graph.set_xlim(xmin=0, xmax=days)
return fig
def addedRecently(self, numdays=30, attr='created'):
days = {}
fig = Figure(figsize=(self.width, self.height), dpi=self.dpi)
limit = time.time() - numdays * 86400
res = self.deck.s.column0("select %s from cards where %s >= %f" %
(attr, attr, limit))
for r in res:
d = (r - self.startOfDay) / 86400.0
days[round(d)] = days.get(round(d), 0) + 1
self.addMissing(days, -numdays, 0)
graph = fig.add_subplot(111)
intervals = self.unzip(days.items())
if attr == 'created':
colour = "#ffaaaa"
else:
colour = "#ffcccc"
self.filledGraph(graph, numdays, intervals[0], intervals[1], colour)
graph.set_ylabel(_("Cards"))
graph.set_xlabel(_("Day"))
graph.set_xlim(xmin=-numdays, xmax=0)
return fig
def addMissing(self, dict, min, max):
for i in range(min, max+1):
if not i in dict:
dict[i] = 0
def unzip(self, tuples, fillFix=True):
tuples.sort(cmp=lambda x,y: cmp(x[0], y[0]))
new = zip(*tuples)
return new
def filledGraph(self, graph, days, x=(), y=(), c="b"):
x = list(x)
y = list(y)
lowest = 99999
highest = -lowest
for i in range(len(x)):
if x[i] < lowest:
lowest = x[i]
if x[i] > highest:
highest = x[i]
# ensure the filled area reaches the bottom
x.insert(0, lowest - 1)
y.insert(0, 0)
x.append(highest + 1)
y.append(0)
# plot
graph.fill(x, y, c)
if days < 180:
graph.bar(x, y, width=0)
lw=3
else:
lw=1
graph.plot(x, y, "k", lw=lw)
graph.grid(True)
graph.set_ylim(ymin=0, ymax=max(2, graph.get_ylim()[1]))
def easeBars(self):
fig = Figure(figsize=(3, 3), dpi=self.dpi)
graph = fig.add_subplot(111)
types = ("new", "young", "mature")
enum = 5
offset = 0
arrsize = 17
arr = [0] * arrsize
n = 0
colours = ["#ff7777", "#77ffff", "#7777ff"]
bars = []
gs = anki.stats.globalStats(self.deck.s)
for type in types:
total = (getattr(gs, type + "Ease0") +
getattr(gs, type + "Ease1") +
getattr(gs, type + "Ease2") +
getattr(gs, type + "Ease3") +
getattr(gs, type + "Ease4"))
for e in range(enum):
try:
arr[e+offset] = (getattr(gs, type + "Ease%d" % e)
/ float(total)) * 100 + 1
except ZeroDivisionError:
arr[e+offset] = 0
bars.append(graph.bar(range(arrsize), arr, width=1.0,
color=colours[n], align='center'))
arr = [0] * arrsize
offset += 6
n += 1
graph.set_ylabel("%")
x = ([""] + [str(n) for n in range(enum)]) * 3
del x[0]
graph.legend([p[0] for p in bars], (_("New cards"),
_("Young cards"),
_("Mature cards")),
'upper left')
graph.set_ylim(ymax=100)
graph.set_xticks(range(arrsize))
graph.set_xticklabels(x)
graph.grid(True)
return fig

70
anki/history.py Normal file
View file

@ -0,0 +1,70 @@
# -*- coding: utf-8 -*-
# Copyright: Damien Elmes <anki@ichi2.net>
# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html
"""\
History - keeping a record of all reviews
==========================================
"""
__docformat__ = 'restructuredtext'
import time
from anki.db import *
reviewHistoryTable = Table(
'reviewHistory', metadata,
Column('id', Integer, primary_key=True),
Column('cardId', Integer, ForeignKey("cards.id")),
Column('time', Float, nullable=False, default=time.time),
Column('lastInterval', Float, nullable=False),
Column('nextInterval', Float, nullable=False),
Column('ease', Integer, nullable=False),
Column('delay', Float, nullable=False),
Column('lastFactor', Float, nullable=False),
Column('nextFactor', Float, nullable=False),
Column('reps', Float, nullable=False),
Column('thinkingTime', Float, nullable=False),
Column('yesCount', Float, nullable=False),
Column('noCount', Float, nullable=False))
class CardHistoryEntry(object):
"Create after rescheduling card."
def __init__(self, card=None, ease=None, delay=None):
if not card:
return
self.cardId = card.id
self.lastInterval = card.lastInterval
self.nextInterval = card.interval
self.lastFactor = card.lastFactor
self.nextFactor = card.factor
self.reps = card.reps
self.yesCount = card.yesCount
self.noCount = card.noCount
self.ease = ease
self.delay = delay
self.thinkingTime = card.thinkingTime()
def writeSQL(self, s):
s.statement("""
insert into reviewHistory
(cardId, lastInterval, nextInterval, ease, delay, lastFactor,
nextFactor, reps, thinkingTime, yesCount, noCount, time)
values (
:cardId, :lastInterval, :nextInterval, :ease, :delay,
:lastFactor, :nextFactor, :reps, :thinkingTime, :yesCount, :noCount,
:time)""",
cardId=self.cardId,
lastInterval=self.lastInterval,
nextInterval=self.nextInterval,
ease=self.ease,
delay=self.delay,
lastFactor=self.lastFactor,
nextFactor=self.nextFactor,
reps=self.reps,
thinkingTime=self.thinkingTime,
yesCount=self.yesCount,
noCount=self.noCount,
time=time.time())
mapper(CardHistoryEntry, reviewHistoryTable)

231
anki/importing/__init__.py Normal file
View file

@ -0,0 +1,231 @@
# -*- coding: utf-8 -*-
# Copyright: Damien Elmes <anki@ichi2.net>
# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html
"""\
Importing support
==============================
To import, a mapping is created of the form: [FieldModel, ...]. The mapping
may be extended by calling code if a file has more fields. To ignore a
particular FieldModel, replace it with None. The same field model should not
occur more than once."""
__docformat__ = 'restructuredtext'
import time
from anki.cards import cardsTable
from anki.facts import factsTable, fieldsTable
from anki.lang import _
from anki.utils import genID
from anki.errors import *
# Base importer
##########################################################################
class ForeignCard(object):
"An temporary object storing fields and attributes."
def __init__(self):
self.fields = []
self.tags = u""
class Importer(object):
needMapper = True
tagDuplicates = False
multipleCardsAllowed = True
def __init__(self, deck, file):
self.file = file
self._model = deck.currentModel
self._mapping = None
self.log = []
self.deck = deck
self.total = 0
self.tagsToAdd = u""
def doImport(self):
"Import."
c = self.foreignCards()
self.importCards(c)
if c:
self.deck.setModified()
def fields(self):
"The number of fields."
return 0
def foreignCards(self):
"Return a list of foreign cards for importing."
assert 0
def resetMapping(self):
"Reset mapping to default."
numFields = self.fields()
m = []
[m.append(f) for f in self.model.fieldModels if f.required]
[m.append(f) for f in self.model.fieldModels if not f.required]
rem = max(0, self.fields() - len(m))
m += [None] * rem
del m[numFields:]
self._mapping = m
def getMapping(self):
if not self._mapping:
self.resetMapping()
return self._mapping
def setMapping(self, mapping):
self._mapping = mapping
mapping = property(getMapping, setMapping)
def getModel(self):
return self._model
def setModel(self, model):
self._model = model
# update the mapping for the new model
self._mapping = None
self.getMapping()
model = property(getModel, setModel)
def importCards(self, cards):
"Convert each card into a fact, apply attributes and add to deck."
# ensure all unique and required fields are mapped
for fm in self.model.fieldModels:
if fm.required or fm.unique:
if fm not in self.mapping:
raise ImportFormatError(
type="missingRequiredUnique",
info=_("Missing required/unique field '%(field)s'") %
{'field': fm.name})
active = 0
for cm in self.model.cardModels:
if cm.active: active += 1
if active > 1 and not self.multipleCardsAllowed:
raise ImportFormatError(type="tooManyCards",
info=_("""
The current importer only supports a single active card model. Please disable
all but one card model."""))
# strip invalid cards
cards = self.stripInvalid(cards)
cards = self.stripOrTagDupes(cards)
if cards:
self.addCards(cards)
def addCards(self, cards):
"Add facts in bulk from foreign cards."
# add facts
factIds = [genID() for n in range(len(cards))]
self.deck.s.execute(factsTable.insert(),
[{'modelId': self.model.id,
'tags': self.tagsToAdd,
'id': factIds[n]} for n in range(len(cards))])
self.deck.s.execute("""
delete from factsDeleted
where factId in (%s)""" % ",".join([str(s) for s in factIds]))
# add all the fields
for fm in self.model.fieldModels:
try:
index = self.mapping.index(fm)
except ValueError:
index = None
data = [{'factId': factIds[m],
'fieldModelId': fm.id,
'ordinal': fm.ordinal,
'id': genID(),
'value': (index is not None and
cards[m].fields[index] or u"")}
for m in range(len(cards))]
self.deck.s.execute(fieldsTable.insert(),
data)
# and cards
now = time.time()
for cm in self.model.cardModels:
self._now = now
if cm.active:
data = [self.addMeta({
'id': genID(),
'factId': factIds[m],
'cardModelId': cm.id,
'ordinal': cm.ordinal,
'question': cm.renderQASQL('q', factIds[m]),
'answer': cm.renderQASQL('a', factIds[m]),
'type': 2},cards[m]) for m in range(len(cards))]
self.deck.s.execute(cardsTable.insert(),
data)
self.total = len(factIds)
def addMeta(self, data, card):
"Add any scheduling metadata to cards"
if 'fields' in card.__dict__:
del card.fields
data['created'] = self._now
data['modified'] = self._now
data['due'] = self._now
self._now += .00001
data.update(card.__dict__)
return data
def stripInvalid(self, cards):
return [c for c in cards if self.cardIsValid(c)]
def cardIsValid(self, card):
fieldNum = len(card.fields)
for n in range(len(self.mapping)):
if self.mapping[n] and self.mapping[n].required:
if fieldNum <= n or not card.fields[n].strip():
self.log.append("Fact is missing field '%s': %s" %
(self.mapping[n].name,
", ".join(card.fields)))
return False
return True
def stripOrTagDupes(self, cards):
# build a cache of items
self.uniqueCache = {}
for field in self.mapping:
if field and field.unique:
self.uniqueCache[field.id] = self.getUniqueCache(field)
return [c for c in cards if self.cardIsUnique(c)]
def getUniqueCache(self, field):
"Return a dict with all fields, to test for uniqueness."
return dict(self.deck.s.all(
"select value, 1 from fields where fieldModelId = :fmid",
fmid=field.id))
def cardIsUnique(self, card):
fields = []
for n in range(len(self.mapping)):
if self.mapping[n] and self.mapping[n].unique:
if card.fields[n] in self.uniqueCache[self.mapping[n].id]:
if not self.tagDuplicates:
self.log.append("Fact has duplicate '%s': %s" %
(self.mapping[n].name,
", ".join(card.fields)))
return False
fields.append(self.mapping[n].name)
else:
self.uniqueCache[self.mapping[n].id][card.fields[n]] = 1
if fields:
card.tags += u"Import: duplicate, Duplicate: " + (
"+".join(fields))
return True
# Export modules
##########################################################################
from anki.importing.csv import TextImporter
from anki.importing.anki10 import Anki10Importer
from anki.importing.mnemosyne10 import Mnemosyne10Importer
from anki.importing.wcu import WCUImporter
Importers = (
(_("TAB/semicolon-separated file (*.*)"), TextImporter),
(_("Anki 1.0 deck (*.anki)"), Anki10Importer),
(_("Mnemosyne 1.0 deck (*.mem)"), Mnemosyne10Importer),
(_("CueCard deck (*.wcu)"), WCUImporter),
)

285
anki/importing/anki03.py Normal file
View file

@ -0,0 +1,285 @@
# -*- coding: utf-8 -*-
# Copyright: Damien Elmes <anki@ichi2.net>
# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html
"""\
Importing Anki v0.3 decks
==========================
"""
__docformat__ = 'restructuredtext'
import cPickle, cStringIO, os, datetime, types, sys
from anki.models import Model, FieldModel, CardModel
from anki.facts import Fact, factsTable, fieldsTable
from anki.cards import cardsTable
from anki.stats import *
from anki.features import FeatureManager
from anki.importing import Importer
from anki.utils import genID
def transformClasses(m, c):
"Map objects into dummy classes"
class EmptyClass(object):
pass
class EmptyList(list):
pass
class EmptyDict(dict):
pass
if c == "Fact":
return EmptyDict
elif c in ("CardModels", "Deck", "Facts", "Fields", "Models"):
return EmptyList
return EmptyClass
def load(path):
"Load a deck from PATH."
if isinstance(path, types.UnicodeType):
path = path.encode(sys.getfilesystemencoding())
file = open(path, "rb")
try:
try:
data = file.read()
except (IOError, OSError):
raise ImportError
finally:
file.close()
unpickler = cPickle.Unpickler(cStringIO.StringIO(data))
unpickler.find_global = transformClasses
deck = unpickler.load()
deck.path = unicode(os.path.abspath(path), sys.getfilesystemencoding())
return deck
# we need to upgrade the deck before converting
def maybeUpgrade(deck):
# change old hardInterval from 1 day to 8-12 hours
if list(deck.sched.hardInterval) == [1.0, 1.0]:
deck.sched.hardInterval = [0.3333, 0.5]
# add 'medium priority' support
if not hasattr(deck.sched, 'medPriority'):
deck.sched.medPriority = []
# add delay2
if not hasattr(deck.sched, "delay2"):
deck.sched.delay2 = 28800
# add collapsing
if not hasattr(deck.sched, "collapse"):
deck.sched.collapse = 18000
# card related
# - add 'total' attribute
for card in deck:
card.__dict__['total'] = (
card.stats['new']['yes'] +
card.stats['new']['no'] +
card.stats['young']['yes'] +
card.stats['young']['no'] +
card.stats['old']['yes'] +
card.stats['old']['no'])
class Anki03Importer(Importer):
needMapper = False
def doImport(self):
oldDeck = load(self.file)
maybeUpgrade(oldDeck)
# mappings for old->new ids
cardModels = {}
fieldModels = {}
# start with the models
s = self.deck.s
deck = self.deck
import types
def uni(txt):
if txt is None:
return txt
if not isinstance(txt, types.UnicodeType):
txt = unicode(txt, "utf-8")
return txt
for oldModel in oldDeck.facts.models:
model = Model(uni(oldModel.name), uni(oldModel.description))
model.id = oldModel.id
model.tags = u", ".join(oldModel.tags)
model.features = u", ".join(oldModel.decorators)
model.created = oldModel.created
model.modified = oldModel.modified
deck.newCardOrder = min(1, oldModel.position)
deck.addModel(model)
# fields
for oldField in oldModel.fields:
fieldModel = FieldModel(uni(oldField.name),
uni(oldField.description),
oldField.name in oldModel.required,
oldField.name in oldModel.unique)
fieldModel.features = u", ".join(oldField.features)
fieldModel.quizFontFamily = uni(oldField.display['quiz']['fontFamily'])
fieldModel.quizFontSize = oldField.display['quiz']['fontSize']
fieldModel.quizFontColour = uni(oldField.display['quiz']['fontColour'])
fieldModel.editFontFamily = uni(oldField.display['edit']['fontFamily'])
fieldModel.editFontSize = oldField.display['edit']['fontSize']
fieldModel.id = oldField.id
model.addFieldModel(fieldModel)
s.flush() # we need the id
fieldModels[id(oldField)] = fieldModel
# card models
for oldCard in oldModel.allcards:
cardModel = CardModel(uni(oldCard.name),
uni(oldCard.description),
uni(oldCard.qformat),
uni(oldCard.aformat))
cardModel.active = oldCard in oldModel.cards
cardModel.questionInAnswer = oldCard.questionInAnswer
cardModel.id = oldCard.id
model.spacing = 0.25
cardModel.questionFontFamily = uni(oldCard.display['question']['fontFamily'])
cardModel.questionFontSize = oldCard.display['question']['fontSize']
cardModel.questionFontColour = uni(oldCard.display['question']['fontColour'])
cardModel.questionAlign = oldCard.display['question']['align']
cardModel.answerFontFamily = uni(oldCard.display['answer']['fontFamily'])
cardModel.answerFontSize = oldCard.display['answer']['fontSize']
cardModel.answerFontColour = uni(oldCard.display['answer']['fontColour'])
cardModel.answerAlign = oldCard.display['answer']['align']
cardModel.lastFontFamily = uni(oldCard.display['last']['fontFamily'])
cardModel.lastFontSize = oldCard.display['last']['fontSize']
cardModel.lastFontColour = uni(oldCard.display['last']['fontColour'])
cardModel.editQuestionFontFamily = (
uni(oldCard.display['editQuestion']['fontFamily']))
cardModel.editQuestionFontSize = (
oldCard.display['editQuestion']['fontSize'])
cardModel.editAnswerFontFamily = (
uni(oldCard.display['editAnswer']['fontFamily']))
cardModel.editAnswerFontSize = (
oldCard.display['editAnswer']['fontSize'])
model.addCardModel(cardModel)
s.flush() # we need the id
cardModels[id(oldCard)] = cardModel
# facts
def getSpace(lastCard, lastAnswered):
if not lastCard:
return 0
return lastAnswered + lastCard.delay
def getLastCardId(fact):
if not fact.lastCard:
return None
ret = [c.id for c in fact.cards if c.model.id == fact.lastCard.id]
if ret:
return ret[0]
d = [{'id': f.id,
'modelId': f.model.id,
'created': f.created,
'modified': f.modified,
'tags': u",".join(f.tags),
'spaceUntil': getSpace(f.lastCard, f.lastAnswered),
'lastCardId': getLastCardId(f)
} for f in oldDeck.facts]
if d:
s.execute(factsTable.insert(), d)
self.total = len(oldDeck.facts)
# fields in facts
toAdd = []
for oldFact in oldDeck.facts:
for field in oldFact.model.fields:
toAdd.append({'factId': oldFact.id,
'id': genID(),
'fieldModelId': fieldModels[id(field)].id,
'ordinal': fieldModels[id(field)].ordinal,
'value': uni(oldFact.get(field.name, u""))})
if toAdd:
s.execute(fieldsTable.insert(), toAdd)
# cards
class FakeObj(object):
pass
fake = FakeObj()
fake.fact = FakeObj()
fake.fact.model = FakeObj()
fake.cardModel = FakeObj()
def renderQA(c, type):
fake.tags = u", ".join(c.tags)
fake.fact.tags = u", ".join(c.fact.tags)
fake.fact.model.tags = u", ".join(c.fact.model.tags)
fake.cardModel.name = c.model.name
return cardModels[id(c.model)].renderQA(fake, c.fact, type)
d = [{'id': c.id,
'created': c.created,
'modified': c.modified,
'factId': c.fact.id,
'ordinal': cardModels[id(c.model)].ordinal,
'cardModelId': cardModels[id(c.model)].id,
'tags': u", ".join(c.tags),
'factor': 2.5,
'firstAnswered': c.firstAnswered,
'interval': c.interval,
'lastInterval': c.lastInterval,
'modified': c.modified,
'due': c.nextTime,
'lastDue': c.lastTime,
'reps': c.total,
'question': renderQA(c, 'question'),
'answer': renderQA(c, 'answer'),
'averageTime': c.stats['averageTime'],
'reviewTime': c.stats['totalTime'],
'yesCount': (c.stats['new']['yes'] +
c.stats['young']['yes'] +
c.stats['old']['yes']),
'noCount': (c.stats['new']['no'] +
c.stats['young']['no'] +
c.stats['old']['no']),
'successive': c.stats['successivelyCorrect']}
for c in oldDeck]
if d:
s.execute(cardsTable.insert(), d)
# scheduler
deck.description = uni(oldDeck.description)
deck.created = oldDeck.created
deck.maxScheduleTime = oldDeck.sched.maxScheduleTime
deck.hardIntervalMin = oldDeck.sched.hardInterval[0]
deck.hardIntervalMax = oldDeck.sched.hardInterval[1]
deck.midIntervalMin = oldDeck.sched.midInterval[0]
deck.midIntervalMax = oldDeck.sched.midInterval[1]
deck.easyIntervalMin = oldDeck.sched.easyInterval[0]
deck.easyIntervalMax = oldDeck.sched.easyInterval[1]
deck.delay0 = oldDeck.sched.delay0
deck.delay1 = oldDeck.sched.delay1
deck.delay2 = oldDeck.sched.delay2
deck.collapseTime = 3600 # oldDeck.sched.collapse
deck.highPriority = u", ".join(oldDeck.sched.highPriority)
deck.medPriority = u", ".join(oldDeck.sched.medPriority)
deck.lowPriority = u", ".join(oldDeck.sched.lowPriority)
deck.suspended = u", ".join(oldDeck.sched.suspendedTags)
# scheduler global stats
stats = Stats()
stats.create(deck.s, 0, datetime.date.today())
stats.day = datetime.date.fromtimestamp(oldDeck.created)
stats.averageTime = oldDeck.sched.globalStats['averageTime']
stats.reviewTime = oldDeck.sched.globalStats['totalTime']
stats.distractedTime = 0
stats.distractedReps = 0
stats.newEase0 = oldDeck.sched.easeStats.get('new', {}).get(0, 0)
stats.newEase1 = oldDeck.sched.easeStats.get('new', {}).get(1, 0)
stats.newEase2 = oldDeck.sched.easeStats.get('new', {}).get(2, 0)
stats.newEase3 = oldDeck.sched.easeStats.get('new', {}).get(3, 0)
stats.newEase4 = oldDeck.sched.easeStats.get('new', {}).get(4, 0)
stats.youngEase0 = oldDeck.sched.easeStats.get('young', {}).get(0, 0)
stats.youngEase1 = oldDeck.sched.easeStats.get('young', {}).get(1, 0)
stats.youngEase2 = oldDeck.sched.easeStats.get('young', {}).get(2, 0)
stats.youngEase3 = oldDeck.sched.easeStats.get('young', {}).get(3, 0)
stats.youngEase4 = oldDeck.sched.easeStats.get('young', {}).get(4, 0)
stats.matureEase0 = oldDeck.sched.easeStats.get('old', {}).get(0, 0)
stats.matureEase1 = oldDeck.sched.easeStats.get('old', {}).get(1, 0)
stats.matureEase2 = oldDeck.sched.easeStats.get('old', {}).get(2, 0)
stats.matureEase3 = oldDeck.sched.easeStats.get('old', {}).get(3, 0)
stats.matureEase4 = oldDeck.sched.easeStats.get('old', {}).get(4, 0)
yesCount = (oldDeck.sched.globalStats['new']['yes'] +
oldDeck.sched.globalStats['young']['yes'] +
oldDeck.sched.globalStats['old']['yes'])
noCount = (oldDeck.sched.globalStats['new']['no'] +
oldDeck.sched.globalStats['young']['no'] +
oldDeck.sched.globalStats['old']['no'])
stats.reps = yesCount + noCount
stats.toDB(deck.s)
# ignore daily stats & history, they make no sense on new version
s.flush()
deck.updateAllPriorities()
# save without updating mod time
deck.modified = oldDeck.modified
deck.lastLoaded = deck.modified
deck.s.commit()
deck.save()

58
anki/importing/anki10.py Normal file
View file

@ -0,0 +1,58 @@
# -*- coding: utf-8 -*-
# Copyright: Damien Elmes <anki@ichi2.net>
# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html
"""\
Importing Anki 0.9+ decks
==========================
"""
__docformat__ = 'restructuredtext'
from anki import DeckStorage
from anki.importing import Importer
from anki.sync import SyncClient, SyncServer
class Anki10Importer(Importer):
needMapper = False
def doImport(self):
"Import."
src = DeckStorage.Deck(self.file)
client = SyncClient(self.deck)
server = SyncServer(src)
client.setServer(server)
# if there is a conflict, sync local -> src
client.localTime = self.deck.modified
client.remoteTime = 0
src.s.execute("update facts set modified = 1")
src.s.execute("update models set modified = 1")
src.s.execute("update cards set modified = 1")
src.s.execute("update media set created = 1")
self.deck.s.flush()
# set up a custom change list and sync
lsum = client.summary(0)
self._clearDeleted(lsum)
rsum = server.summary(0)
self._clearDeleted(rsum)
payload = client.genPayload((lsum, rsum))
# no need to add anything to src
payload['added-models'] = []
payload['added-cards'] = []
payload['added-facts'] = {'facts': [], 'fields': []}
assert payload['deleted-facts'] == []
assert payload['deleted-cards'] == []
assert payload['deleted-models'] == []
res = server.applyPayload(payload)
client.applyPayloadReply(res)
# add tags
fids = [f[0] for f in res['added-facts']['facts']]
self.deck.addFactTags(fids, self.tagsToAdd)
self.total = len(res['added-facts']['facts'])
src.s.rollback()
self.deck.flushMod()
def _clearDeleted(self, sum):
sum['delcards'] = []
sum['delfacts'] = []
sum['delmodels'] = []

129
anki/importing/csv.py Normal file
View file

@ -0,0 +1,129 @@
# -*- coding: utf-8 -*-
# Copyright: Damien Elmes <anki@ichi2.net>
# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html
"""\
Importing CSV/TSV files
========================
"""
__docformat__ = 'restructuredtext'
import codecs
from anki.importing import Importer, ForeignCard
from anki.lang import _
from anki.errors import *
class TextImporter(Importer):
patterns = ("\t", ";")
def __init__(self, *args):
Importer.__init__(self, *args)
self.lines = None
def foreignCards(self):
self.parseTopLine()
# process all lines
log = []
cards = []
lineNum = 0
ignored = 0
for line in self.lines:
lineNum += 1
if not line.strip():
# ignore blank lines
continue
try:
fields = self.parseLine(line)
except ValueError:
log.append(_("Line %(line)d doesn't match pattern '%(pat)s'")
% {
'line': lineNum,
'pat': pattern,
})
ignored += 1
continue
if len(fields) != self.numFields:
log.append(_(
"Line %(line)d had %(num1)d fields,"
" expected %(num2)d") % {
"line": lineNum,
"num1": len(fields),
"num2": self.numFields,
})
ignored += 1
continue
card = self.cardFromFields(fields)
cards.append(card)
self.log = log
self.ignored = ignored
return cards
def parseTopLine(self):
"Parse the top line and determine the pattern and number of fields."
# load & look for the right pattern
self.cacheFile()
# look for the first non-blank line
l = None
for line in self.lines:
ret = line.strip()
if ret:
l = line
break
if not l:
raise ImportFormatError(type="emptyFile",
info=_("The file had no non-empty lines."))
found = False
for p in self.patterns:
if p in l:
pattern = p
fields = l.split(p)
numFields = len(fields)
found = True
break
if not found:
fmtError = _(
"Couldn't find pattern. The file should be a series "
"of lines separated by tabs or semicolons.")
raise ImportFormatError(type="invalidPattern",
info=fmtError)
self.pattern = pattern
self.setNumFields(line)
def cacheFile(self):
"Read file into self.lines if not already there."
if not self.lines:
self.lines = self.readFile()
def readFile(self):
f = codecs.open(self.file, encoding="utf-8")
try:
data = f.readlines()
except UnicodeDecodeError, e:
raise ImportFormatError(type="encodingError",
info=_("The file was not in UTF8 format."))
if not data:
return []
if data[0].startswith(unicode(codecs.BOM_UTF8, "utf8")):
data[0] = data[0][1:]
# remove comment char
lines = [l for l in data if not l.lstrip().startswith("#")]
return lines
def fields(self):
"Number of fields."
self.parseTopLine()
return self.numFields
def setNumFields(self, line):
self.numFields = len(self.parseLine(line))
def parseLine(self, line):
fields = line.split(self.pattern)
fields = [f.strip() for f in fields]
return fields
def cardFromFields(self, fields):
card = ForeignCard()
card.fields.extend(fields)
return card

View file

@ -0,0 +1,76 @@
# -*- coding: utf-8 -*-
# Copyright: Damien Elmes <anki@ichi2.net>
# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html
"""\
Importing Mnemosyne 1.0 decks
==============================
"""
__docformat__ = 'restructuredtext'
import sys, pickle, time, re
from anki.importing import Importer, ForeignCard
from anki.errors import *
class Mnemosyne10Importer(Importer):
multipleCardsAllowed = False
def foreignCards(self):
# empty objects so we can load the native mnemosyne file
class MnemosyneModule(object):
class StartTime:
pass
class Category:
pass
class Item:
pass
for module in ('mnemosyne',
'mnemosyne.core',
'mnemosyne.core.mnemosyne_core'):
sys.modules[module] = MnemosyneModule()
try:
file = open(self.file, "rb")
except (IOError, OSError), e:
raise ImportFormatError(type="systemError",
info=str(e))
header = file.readline().strip()
if (header != "--- Mnemosyne Data Base --- Format Version 1 ---" and
header != "--- Mnemosyne Data Base --- Format Version 2 ---"):
raise ImportFormatError(type="versionError",
info=header)
# read the structure in
try:
struct = pickle.load(file)
except (EOFError, KeyError):
raise ImportFormatError(type="invalidFile")
startTime = struct[0].time
daysPassed = (time.time() - startTime) / 86400.0
# gather cards
cards = []
for item in struct[2]:
card = ForeignCard()
card.fields.append(self.fudgeText(item.q))
card.fields.append(self.fudgeText(item.a))
# scheduling data
card.interval = item.next_rep - item.last_rep
secDelta = (item.next_rep - daysPassed) * 86400.0
card.due = card.nextTime = time.time() + secDelta
# for some reason mnemosyne starts cards off on 1 instead of 0
card.successive = max(
(item.acq_reps_since_lapse + item.ret_reps_since_lapse -1), 0)
card.yesCount = (item.acq_reps + item.ret_reps) - 1
card.noCount = item.lapses
card.reps = card.yesCount + card.noCount
if item.cat.name != u"<default>":
card.tags = item.cat.name
cards.append(card)
return cards
def fields(self):
return 2
def fudgeText(self, text):
text = text.replace("\n", "<br>")
text = re.sub('<sound src="(.*?)">', '[sound:\\1]', text)
return text

57
anki/importing/wcu.py Normal file
View file

@ -0,0 +1,57 @@
# -*- coding: utf-8 -*-
# Author Chris Aakre <caaakre@gmail.com>
# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html
"""\
Importing WCU files
====================
"""
__docformat__ = 'restructuredtext'
import codecs
from anki.importing import Importer, ForeignCard
from anki.lang import _
from anki.errors import *
class WCUImporter(Importer):
def __init__(self, *args):
Importer.__init__(self, *args)
self.lines = None
self.numFields=int(2)
def foreignCards(self):
from xml.dom import minidom, Node
cards = []
f = None
try:
f = codecs.open(self.file, encoding="utf-8")
except:
raise ImportFormatError(type="encodingError", info=_("The file was not in UTF8 format."))
f.close()
def wcuwalk(parent, cards, level=0):
for node in parent.childNodes:
if node.nodeType == Node.ELEMENT_NODE:
myCard=ForeignCard()
if node.attributes.has_key("QuestionPicture"):
question = [unicode('<img src="'+node.attributes.get("QuestionPicture").nodeValue+'"><br/>'+node.attributes.get("Question").nodeValue)]
else:
question = [unicode(node.attributes.get("Question").nodeValue)]
if node.attributes.has_key("AnswerPicture"):
answer = [unicode('<img src="'+node.attributes.get("AnswerPicture").nodeValue+'"><br/>'+node.attributes.get("Answer").nodeValue)]
else:
answer = [unicode(node.attributes.get("Answer").nodeValue)]
myCard.fields.extend(question)
myCard.fields.extend(answer)
cards.append(myCard)
wcuwalk(node, cards, level+1)
def importwcu(file):
wcuwalk(minidom.parse(file).documentElement,cards)
importwcu(self.file)
return cards
def fields(self):
return self.numFields
def setNumFields(self):
self.numFields = int(2)

59
anki/lang.py Normal file
View file

@ -0,0 +1,59 @@
# -*- coding: utf-8 -*-
# Copyright: Damien Elmes <anki@ichi2.net>
# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html
"""\
Internationalisation
=====================
"""
__docformat__ = 'restructuredtext'
import os, sys
import gettext
import threading
threadLocal = threading.local()
# global defaults
currentLang = None
currentTranslation = None
def localTranslation():
"Return the translation local to this thread, or the default."
if getattr(threadLocal, 'currentTranslation', None):
return threadLocal.currentTranslation
else:
return currentTranslation
def _(str):
return localTranslation().ugettext(str)
def ngettext(single, plural, n):
return localTranslation().ungettext(single, plural, n)
def setLang(lang, local=True):
base = os.path.dirname(os.path.abspath(__file__))
localeDir = os.path.join(base, "locale")
if not os.path.exists(localeDir):
localeDir = os.path.join(
os.path.dirname(sys.argv[0]), "locale")
trans = gettext.translation('libanki', localeDir,
languages=[lang],
fallback=True)
if local:
threadLocal.currentLang = lang
threadLocal.currentTranslation = trans
else:
global currentLang, currentTranslation
currentLang = lang
currentTranslation = trans
def getLang():
"Return the language local to this thread, or the default."
if getattr(threadLocal, 'currentLang', None):
return threadLocal.currentLang
else:
return currentLang
if not currentTranslation:
setLang("en_US", local=False)

113
anki/latex.py Normal file
View file

@ -0,0 +1,113 @@
# -*- coding: utf-8 -*-
# Copyright: Damien Elmes <anki@ichi2.net>
# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html
"""\
Latex support
==============================
"""
__docformat__ = 'restructuredtext'
import re, tempfile, os, sys, subprocess
from htmlentitydefs import entitydefs
try:
import hashlib
md5 = hashlib.md5
except ImportError:
import md5
md5 = md5.new
latexPreamble = ("\\documentclass[12pt]{article}\n"
"\\special{papersize=3in,5in}"
"\\usepackage[utf8]{inputenc}"
"\\pagestyle{empty}\n"
"\\begin{document}")
latexPostamble = "\\end{document}"
latexDviPngCmd = ["dvipng", "-D", "200", "-T", "tight"]
regexps = {
"standard": re.compile(r"\[latex\](.+?)\[/latex\]", re.DOTALL | re.IGNORECASE),
"expression": re.compile(r"\[\$\](.+?)\[/\$\]", re.DOTALL | re.IGNORECASE),
"math": re.compile(r"\[\$\$\](.+?)\[/\$\$\]", re.DOTALL | re.IGNORECASE),
}
tmpdir = tempfile.mkdtemp(prefix="anki-latex")
# add standard tex install location to osx
if sys.platform == "darwin":
os.environ['PATH'] += ":/usr/texbin"
def renderLatex(deck, text):
"Convert TEXT with embedded latex tags to image links."
for match in regexps['standard'].finditer(text):
text = text.replace(match.group(), imgLink(deck, match.group(1)))
for match in regexps['expression'].finditer(text):
text = text.replace(match.group(), imgLink(
deck, "$" + match.group(1) + "$"))
for match in regexps['math'].finditer(text):
text = text.replace(match.group(), imgLink(
deck,
"\\begin{displaymath}" + match.group(1) + "\\end{displaymath}"))
return text
def stripLatex(text):
for match in regexps['standard'].finditer(text):
text = text.replace(match.group(), "")
for match in regexps['expression'].finditer(text):
text = text.replace(match.group(), "")
for match in regexps['math'].finditer(text):
text = text.replace(match.group(), "")
return text
def call(*args, **kwargs):
try:
o = subprocess.Popen(*args, **kwargs)
except OSError:
# command not found
return -1
while 1:
try:
ret = o.wait()
except OSError:
# interrupted system call
continue
break
return ret
def imgLink(deck, latex):
"Parse LATEX and return a HTML image representing the output."
for match in re.compile("&([a-z]+);", re.IGNORECASE).finditer(latex):
if match.group(1) in entitydefs:
latex = latex.replace(match.group(), entitydefs[match.group(1)])
latex = re.sub("<br( /)?>", "\n", latex)
latex = latex.encode("utf-8")
imageFile = "latex-%s.png" % md5(latex).hexdigest()
imagePath = os.path.join(deck.mediaDir(create=True), imageFile)
imagePath = imagePath.encode(sys.getfilesystemencoding())
if not os.path.exists(imagePath):
log = open(os.path.join(tmpdir, "latex_log.txt"), "w+")
texpath = os.path.join(tmpdir, "tmp.tex")
texfile = file(texpath, "w")
texfile.write(latexPreamble + "\n")
texfile.write(latex + "\n")
texfile.write(latexPostamble + "\n")
texfile.close()
texpath = texpath.encode(sys.getfilesystemencoding())
oldcwd = os.getcwd()
if sys.platform == "win32":
si = subprocess.STARTUPINFO()
si.dwFlags |= subprocess.STARTF_USESHOWWINDOW
else:
si = None
try:
os.chdir(tmpdir)
errmsg = _("Error executing 'latex' or 'dvipng' - are they installed?")
if call(["latex", "-interaction=nonstopmode",
texpath], stdout=log, stderr=log, startupinfo=si):
return errmsg
if call(latexDviPngCmd + ["tmp.dvi", "-o", imagePath],
stdout=log, stderr=log, startupinfo=si):
return errmsg
finally:
os.chdir(oldcwd)
return '<img src="%s">' % imageFile

View file

@ -0,0 +1,744 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2008-09-23 03:21+0900\n"
"PO-Revision-Date: 2008-01-01 19:00+0100\n"
"Last-Translator: Michal Čadil <mcadil@gmail.com>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n>1;\n"
#: deck.py:483
#, python-format
msgid ""
"\n"
"<h1>Congratulations!</h1>You have finished the deck for now.<br><br>\n"
"%(next)s\n"
"<br><br>\n"
"- There are <b>%(waiting)d</b>\n"
"<a href=\"http://ichi2.net/anki/wiki/Key_Terms_and_Concepts#head-59a81e35b6afb23930005e943068945214d194b3\">\n"
"spaced</a> cards.<br>\n"
"- There are <b>%(suspended)d</b>\n"
"<a href=\"http://ichi2.net/anki/wiki/Key_Terms_and_Concepts#head-37d2db274e6caa23aef55e29655a6b806901774b\">\n"
"suspended</a> cards."
msgstr ""
#: stdmodels.py:121
msgid ""
"\n"
"A format suitable for Heisig's \"Remembering the Kanji\".\n"
"You are tested from the keyword to the kanji.\n"
"\n"
"Layout of the test is based on the great work at\n"
"http://kanji.koohii.com/\n"
"\n"
"The link in the question will list user-contributed\n"
"stories. A free login is required."
msgstr ""
"\n"
"Formát vhodný pro Heisigovo \"Remembering the Kanji\".\n"
"Jste testováni z převodu slova na kanji.\n"
"\n"
"Rozvržení testu je založeno na skvělé práci na\n"
"http://kanji.koohii.com/\n"
"\n"
"Odkaz v otázce bude zobrazovat příběhy zaslané\n"
"uživateli. Je požadováno přihlášení, které je zdarma."
#: stdmodels.py:101
msgid ""
"\n"
"Enter the English expression you want to learn in the 'Expression' field.\n"
"Enter a description in Japanese or English in the 'Meaning' field."
msgstr ""
"\n"
"Vložte anglický výraz, který se chcete naučit do pole 'Výraz'.\n"
"Vložte popis v japonštině nebo angličtině do pole 'Význam'."
#: stdmodels.py:52
msgid ""
"\n"
"The reading field is automatically generated by default,\n"
"and shows the reading for the expression. For words that\n"
"are normally written in hiragana or katakana and don't\n"
"need a reading, you can put the word in the expression\n"
"field, and leave the reading field blank. A reading will\n"
"will not automatically be generated for words written\n"
"in only hiragana or katakana.\n"
"\n"
"Note that the automatic generation of meaning is not\n"
"perfect, and should be checked before adding cards."
msgstr ""
"\n"
"Pole čtení je ve výchozím nastavení automaticky generováno a\n"
"ukazuje výslovnost výrazu. Pro slova, která jsou normálně psána\n"
"v hiragana nebo katakana a nevyžadují výslovnost, můžete vložit\n"
"slovo do pole významu a pole čtení nechat prázdné.\n"
"Výslovnost nebude automaticky generována pouze pro slova psaná\n"
"v hiragana nebo katakana.\n"
"\n"
"Automatické generování významu není bezchybné a před vložením\n"
"karty by měl být význam překontrolován."
#: stats.py:500
#, python-format
msgid "%(count)s %(gradename)s kanji."
msgstr "%(count)s %(gradename)s kanji."
#: stats.py:498
#, python-format
msgid "%(gradename)s: %(count)s of %(total)s (%(percent)0.1f%%)."
msgstr ""
#: stats.py:277 stats.py:279
#, python-format
msgid "%0.1f seconds"
msgstr "%0.1f sekund"
#: stats.py:262 stats.py:303
#, python-format
msgid "%s ago"
msgstr "%s dní"
#: utils.py:19
#, python-format
msgid "%s day"
msgid_plural "%s days"
msgstr[0] "%s den"
msgstr[1] "%s dní"
#: utils.py:20
#, python-format
msgid "%s hour"
msgid_plural "%s hours"
msgstr[0] "%s hodina"
msgstr[1] "%s hodin"
#: utils.py:21
#, python-format
msgid "%s minute"
msgid_plural "%s minutes"
msgstr[0] "%s minuta"
msgstr[1] "%s minut"
#: utils.py:18
#, python-format
msgid "%s month"
msgid_plural "%s months"
msgstr[0] "%s měsíc"
msgstr[1] "%s měsíců"
#: utils.py:22
#, python-format
msgid "%s second"
msgid_plural "%s seconds"
msgstr[0] "%s sekunda"
msgstr[1] "%s sekund"
#: utils.py:17
#, python-format
msgid "%s year"
msgid_plural "%s years"
msgstr[0] "%s rok"
msgstr[1] "%s let"
#: stats.py:370 stats.py:373 stats.py:376 stats.py:379
#, fuzzy, python-format
msgid "<b>%(a)d</b> (<b>%(b)0.1f</b>/day)"
msgstr "<br>Karty starší než týden: <b>%(old)d</b> (<b>%(oldP)0.2f%%</b>)<br>"
#: stats.py:367
#, fuzzy, python-format
msgid "<b>%(a)d</b>/day, <b>%(b)d</b>/mon"
msgstr "Přidané karty: <b>%(a)d</b> denně, <b>%(b)d</b> měsíčně<br>"
#: stats.py:356
#, fuzzy, python-format
msgid "<b>%0.0f</b> days"
msgstr "Průměrný čas dalšího opakování: <b>%0.0f</b> dní<br/>"
#: stats.py:357 stats.py:359 stats.py:361 stats.py:363 stats.py:365
#, fuzzy, python-format
msgid "<b>%0.1f</b> cards/day"
msgstr "Průměrná zátěž: <b>%0.1f</b> karet denně<br/>"
#: stats.py:354
msgid "<b>Averages</b><br>"
msgstr ""
#: stats.py:333
msgid "<b>Card counts</b><br>"
msgstr ""
#: stats.py:341
msgid "<b>Correct answers</b><br>"
msgstr ""
#: stats.py:522
#, fuzzy, python-format
msgid "<h1>Kanji statistics</h1>The %d seen cards in this deck contain:"
msgstr "<h1>Statistika kanji</h1>%d karet v tomto balíku obsahuje:"
#: stats.py:526
#, python-format
msgid "<li>%d total unique kanji.</li>"
msgstr "<li>%d celkem unikátních kanji.<"
#: sync.py:171
#, python-format
msgid ""
"<table width=500>\n"
"<tr><td><b>Added/Changed</b></td><td><b>Here</b></td><td><b>Server</b></td></tr>\n"
"<tr><td>Cards</td><td>%(lc)d</td><td>%(rc)d</td></tr>\n"
"<tr><td>Facts</td><td>%(lf)d</td><td>%(rf)d</td></tr>\n"
"<tr><td>Models</td><td>%(lm)d</td><td>%(rm)d</td></tr>\n"
"%(media)s\n"
"</table>"
msgstr ""
#: stdmodels.py:32
msgid ""
"A basic flashcard with a front and a back.\n"
"Questions are asked from front to back by default.\n"
"\n"
"Please consider customizing this model, rather than\n"
"using it verbatim: field names like \"expression\" are\n"
"clearer than \"front\" and \"back\", and will ensure\n"
"that your entries are consistent."
msgstr ""
"Běžná kartička s přední a zadní stranou.\n"
"Ve výchozím nastavení se ukazuje zadní strana\n"
"a hádá se přední.\n"
"Prosím zvaže úpravu tohoto modelu spíše než jeho použití\n"
"tak jak je nadefinován: názvy polí jako \"výraz\" jsou\n"
"jasnější než \"přední a zadní strana\" a zajistí, že Vaše\n"
"kartičky budou konzistentní."
#: stdmodels.py:164
msgid "A description in your native language, or Cantonese"
msgstr "Popis ve Vašem jazyce nebo v kantonštině"
#: stdmodels.py:75
msgid "A description in your native language, or Japanese"
msgstr "Popis ve Vašem jazyce nebo v japonštině"
#: stdmodels.py:192
msgid "A description in your native language, or Mandarin"
msgstr "Popis ve Vašem jazyce nebo v mandarínštině"
#: stdmodels.py:38
msgid "A question."
msgstr "Otázka."
#: stdmodels.py:159 stdmodels.py:186
msgid "A word or expression written in Hanzi."
msgstr "Slovo nebo výraz psaný v hanzi."
#: stdmodels.py:65
msgid "A word or expression written in Kanji."
msgstr "Slovo nebo výraz psaný v kanji."
#: stdmodels.py:84 stdmodels.py:170 stdmodels.py:199
msgid "Actively test your recall by producing the target expression"
msgstr "Testujte aktivně Vaše znalosti vytvořením výrazu"
#: stats.py:256
msgid "Added"
msgstr "Přidáno"
#: stats.py:373
msgid "Added last month"
msgstr ""
#: stats.py:370
msgid "Added last week"
msgstr ""
#: exporting.py:216
msgid "Anki deck (*.anki)"
msgstr "Balík anki"
#: exporting.py:55
msgid "Anki decks (*.anki)"
msgstr "Balíky anki"
#: deck.py:418
#, python-format
msgid ""
"At the same time tomorrow:<br><br>\n"
"- There will be <b>%(wait)d</b> cards waiting for review<br>\n"
"- There will be <b>%(new)d</b>\n"
"<a href=\"http://ichi2.net/anki/wiki/Key_Terms_and_Concepts#head-061e5433d4571d7ec7ecba0c329c09bd27c84d63\">\n"
"new cards</a> waiting"
msgstr ""
#: stdmodels.py:78 stdmodels.py:166 stdmodels.py:194
msgid "Automatically generated by default."
msgstr "Ve výchozím nastavení automaticky generováno."
#: stats.py:357
#, fuzzy
msgid "Average reps"
msgstr "Průměrný čas"
#: stats.py:277
msgid "Average time"
msgstr "Průměrný čas"
#: stats.py:367
#, fuzzy
msgid "Avg. added"
msgstr "Přidáno"
#: stdmodels.py:42
msgid "Back to front"
msgstr "Ze zadní strany na přední"
#: stdmodels.py:31
msgid "Basic"
msgstr ""
#: deck.py:1452 deck.py:1525
msgid "Can't read/write deck"
msgstr ""
#: stdmodels.py:156
msgid "Cantonese"
msgstr "kantonština"
#: stats.py:290
msgid "Card model tags"
msgstr "Štítky modelů kartiček"
#: stats.py:289
msgid "Card tags"
msgstr "Štítky kartiček"
#: graphs.py:77 graphs.py:99 graphs.py:113 graphs.py:135
msgid "Cards"
msgstr "Kartičky"
#: exporting.py:217
msgid "Cards in tab-separated text file (*.txt)"
msgstr "Kartičky v textovém souboru odděleném tabulátorem (*.txt)"
#: models.py:25
msgid "Center"
msgstr "Střed"
#: stats.py:259
msgid "Changed"
msgstr "Změněno"
#: stats.py:274
msgid "Correct count"
msgstr "Známé kartičky"
#: stats.py:270
#, fuzzy
msgid "Current factor"
msgstr "Současný interval"
#: stats.py:266
msgid "Current interval"
msgstr "Současný interval"
#: deck.py:1311
msgid "Database file damaged. Restore from backup."
msgstr ""
#: graphs.py:136
msgid "Day"
msgstr ""
#: graphs.py:78 graphs.py:100 graphs.py:114
msgid "Days"
msgstr ""
#: stats.py:318
msgid "Deck Statistics"
msgstr "Statistika balíku"
#: stats.py:319
#, python-format
msgid "Deck created: <b>%s</b> ago<br>"
msgstr "Balík vytvořen před: <b>%s</b><br>"
#: deck.py:1319
msgid "Deck was missing a model"
msgstr ""
#: deck.py:1340
#, python-format
msgid "Deleted %d cards with missing fact"
msgstr ""
#: deck.py:1348
#, python-format
msgid "Deleted %d cards with no card model"
msgstr ""
#: deck.py:1361
#, python-format
msgid "Deleted %d dangling fields"
msgstr ""
#: deck.py:1333
#, python-format
msgid "Deleted %d facts with missing fields"
msgstr ""
#: deck.py:1353
#, python-format
msgid "Deleted %d facts with no cards"
msgstr ""
#: stdmodels.py:100
msgid "English"
msgstr "angličtina"
#: latex.py:104
msgid "Error executing 'latex' or 'dvipng' - are they installed?"
msgstr ""
#: stats.py:294
msgid "Fact tags"
msgstr "Štítky výrazů"
#: exporting.py:218
msgid "Facts in tab-separated text file (*.txt)"
msgstr "výrazy oddělené tabulátory (*.txt)"
#: deck.py:1497
msgid "File is in use by another process"
msgstr ""
#: stats.py:379
#, fuzzy
msgid "First last month"
msgstr "Poprvé"
#: stats.py:376
#, fuzzy
msgid "First last week"
msgstr "Poprvé"
#: stats.py:258
#, fuzzy
msgid "First review"
msgstr "Poprvé"
#: stats.py:282
msgid "First time"
msgstr "Poprvé"
#: stats.py:346
#, fuzzy, python-format
msgid "First-seen cards: <b>%(gNewYes%)0.1f%%</b> (<b>%(gNewYes)d</b> of <b>%(gNewTotal)d</b>)<br><br>"
msgstr "Kartičky známé na první pokus: <b>%(gNewYes%)0.1f%%</b> (<b>%(gNewYes)d</b> of <b>%(gNewTotal)d</b>)<br><br>"
#: stdmodels.py:110
msgid "From the English expression to the meaning."
msgstr "Z anglického výrazu do významu"
#: stdmodels.py:142
msgid "From the keyword to the Kanji."
msgstr "Z klíčového slova do kanji"
#: stdmodels.py:107
msgid "From the meaning to the English expression."
msgstr "Z významu do anglického výrazu"
#: stdmodels.py:40
msgid "Front to back"
msgstr "Ze zadní strany na přední"
#: stdmodels.py:120
msgid "Heisig"
msgstr "Heisig"
#: stats.py:356
#, fuzzy
msgid "Interval"
msgstr "Poslední interval"
#: stdmodels.py:51
msgid "Japanese"
msgstr "japonsky"
#: stats.py:537
msgid "Jouyou levels:"
msgstr "úrovně Jouyou"
#: stats.py:271
msgid "Last factor"
msgstr ""
#: stats.py:269
msgid "Last interval"
msgstr "Poslední interval"
#: models.py:26
msgid "Left"
msgstr "Zbývá"
#: stdmodels.py:183
msgid "Mandarin"
msgstr "mandarínština"
#: stats.py:286
msgid "Mature"
msgstr "Starší karty"
#: graphs.py:210
#, fuzzy
msgid "Mature cards"
msgstr "Starší karty"
#: stats.py:342
#, fuzzy, python-format
msgid "Mature cards: <b>%(gMatureYes%)0.1f%%</b> (<b>%(gMatureYes)d</b> of <b>%(gMatureTotal)d</b>)<br>"
msgstr "Známé starší kartičky: <b>%(gOldYes%)0.1f%%</b> (<b>%(gOldYes)d</b> of <b>%(gOldTotal)d</b>)<br>"
#: stats.py:334
#, fuzzy, python-format
msgid "Mature cards: <b>%(old)d</b> (<b>%(oldP)0.2f%%</b>)<br>"
msgstr "<br>Karty starší než týden: <b>%(old)d</b> (<b>%(oldP)0.2f%%</b>)<br>"
#: media.py:161 media.py:163 media.py:164
msgid "Media Missing"
msgstr ""
#: deck.py:915
#, python-format
msgid "Model '%s' has wrong card model count"
msgstr ""
#: deck.py:919
#, python-format
msgid "Model '%s' has wrong field model count"
msgstr ""
#: stats.py:292
msgid "Model tags"
msgstr "Štítky modelu"
#: graphs.py:208
msgid "New cards"
msgstr "Nové kartičky"
#: stats.py:265
msgid "Next due"
msgstr "Příští opakování"
#: stats.py:316
msgid "Please add some cards first.<p/>"
msgstr "Nejprve vložte nějaké kartičky"
#: stats.py:276
msgid "Repeatedly correct"
msgstr "Opakovaně správně"
#: stats.py:365
#, fuzzy
msgid "Reps last month"
msgstr "%s měsíc"
#: stats.py:363
msgid "Reps last week"
msgstr ""
#: stats.py:361
#, fuzzy
msgid "Reps next month"
msgstr "%s měsíc"
#: stats.py:359
msgid "Reps next week"
msgstr ""
#: stats.py:272
msgid "Review count"
msgstr "Počet opakování"
#: models.py:27
msgid "Right"
msgstr "Správně"
#: deck.py:1824
msgid "Show new cards after all other cards"
msgstr ""
#: deck.py:1818
msgid "Show new cards in order they were added"
msgstr ""
#: deck.py:1817
msgid "Show new cards in random order"
msgstr ""
#: deck.py:1823
msgid "Spread new cards out through reviews"
msgstr ""
#: stats.py:287
msgid "State"
msgstr "Stav"
#: stdmodels.py:88 stdmodels.py:174 stdmodels.py:203
msgid "Test your ability to recognize the target expression"
msgstr "Otestujte Vaši schopnost uhodnout výraz"
#: exporting.py:145 exporting.py:177
msgid "Text files (*.txt)"
msgstr "Textové soubory (*.txt)"
#: stdmodels.py:39
msgid "The answer."
msgstr "Odpověď"
#: deck.py:1325
msgid "The current model didn't exist"
msgstr ""
#: deck.py:431
#, fuzzy
msgid "The deck is empty. Please add some cards."
msgstr "Nejprve vložte nějaké kartičky"
#: deck.py:428
#, python-format
msgid "The next card will be shown in <b>%s</b>"
msgstr ""
#: stats.py:331
#, fuzzy, python-format
msgid "Total number of cards: <b>%d</b><br><br>"
msgstr "Celkový pošet kartiček: <b>%d</b><br>"
#: stats.py:279
msgid "Total time"
msgstr "Celkový čas"
#: deck.py:702
msgid "Unknown"
msgstr "Neznámý"
#: stats.py:338
#, python-format
msgid "Unseen cards: <b>%(new)d</b> (<b>%(newP)0.2f%%</b>)<br><br>"
msgstr "Zatím nezobrazené kartičky: <b>%(new)d</b> (<b>%(newP)0.2f%%</b>)<br><br>"
#: stats.py:284
msgid "Young"
msgstr "Nové"
#: graphs.py:209
msgid "Young cards"
msgstr "Nové kartičky"
#: stats.py:344
#, fuzzy, python-format
msgid "Young cards: <b>%(gYoungYes%)0.1f%%</b> (<b>%(gYoungYes)d</b> of <b>%(gYoungTotal)d</b>)<br>"
msgstr "Známé nové kartičky: <b>%(gYoungYes%)0.1f%%</b> (<b>%(gYoungYes)d</b> of <b>%(gYoungTotal)d</b>)<br>"
#: stats.py:336
#, python-format
msgid "Young cards: <b>%(young)d</b> (<b>%(youngP)0.2f%%</b>)<br>"
msgstr "Nové kartičky: <b>%(young)d</b> (<b>%(youngP)0.2f%%</b>)<br>"
#: models.py:150 models.py:151 models.py:182
msgid "[empty]"
msgstr "[prázdné]"
#: models.py:148
msgid "[invalid format; see model properties]"
msgstr "[neplatný formát; viz vlastnosti modelu]"
#: deck.py:463
msgid "a short time"
msgstr "krátký čas"
#: stats.py:264
#, python-format
msgid "in %s"
msgstr "za %s"
#: deck.py:448
msgid "unknown"
msgstr "neznámý"
#~ msgid "%x at %H:%M"
#~ msgstr "%x v %H:%M"
#~ msgid "Colon/semicolon/tab-separated text file (*.*)"
#~ msgstr "Textový soubor oddělený čárkou/středníkem (*.*)"
#~ msgid "Couldn't find pattern. The file should be a series of lines separated by colons, semicolons or tabs."
#~ msgstr "Vzor nenalezen. Soubor musí být řádky s výrazy oddělenými čárkami, středníky nebo tabulátory."
#~ msgid "Deck%"
#~ msgstr "Balík%"
#~ msgid "Fc .pending file (*.pending *.imported)"
#~ msgstr "Soubor .pendig (*.pending *.imported)"
#~ msgid "Import: duplicate"
#~ msgstr "Import: duplikát"
#~ msgid "Import: missing field"
#~ msgstr "Import: chybějící pole"
#~ msgid "Import: same %s"
#~ msgstr "Import: stejné %s"
#~ msgid "Line %(line)d doesn't match pattern '%(pat)s'"
#~ msgstr "Řádek %(line)d neodpovídá vzoru '%(pat)s"
#~ msgid "Line %(line)d had %(num1)d fields, expected %(num2)d"
#~ msgstr "Řádek %(line)d měl %(num1)d polí, očekáváno %(num2)d"
#~ msgid "Mnemosyne file (*.mem)"
#~ msgstr "Soubor Mnemosyne (*.mem)"
#~ msgid "Model"
#~ msgstr "Model"
#~ msgid "Old Anki 0.2.x file (*.fc)"
#~ msgstr "Soubor Anki verye 0.2.x (*.fc)"
#~ msgid "Old cards"
#~ msgstr "Staré kartičky"
#~ msgid "Order by input date"
#~ msgstr "Seřadit podle data vložení"
#~ msgid "Other"
#~ msgstr "Jiné"
#~ msgid "Period"
#~ msgstr "Období"
#~ msgid "Random"
#~ msgstr "Náhodně"
#~ msgid "The file had no non-empty lines."
#~ msgstr "Pole nemá žádné neprázdné řádky"
#~ msgid "The file was not in UTF8 format."
#~ msgstr "Soubor nebyl ve formátu UTF8"
#~ msgid "Untitled"
#~ msgstr "Bez názvu"
#~ msgid "in %(a)s, on %(b)s"
#~ msgstr "za %(a), %(b)"

View file

@ -0,0 +1,678 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2008-09-23 03:21+0900\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: deck.py:483
#, python-format
msgid ""
"\n"
"<h1>Congratulations!</h1>You have finished the deck for now.<br><br>\n"
"%(next)s\n"
"<br><br>\n"
"- There are <b>%(waiting)d</b>\n"
"<a href=\"http://ichi2.net/anki/wiki/Key_Terms_and_Concepts#head-59a81e35b6afb23930005e943068945214d194b3\">\n"
"spaced</a> cards.<br>\n"
"- There are <b>%(suspended)d</b>\n"
"<a href=\"http://ichi2.net/anki/wiki/Key_Terms_and_Concepts#head-37d2db274e6caa23aef55e29655a6b806901774b\">\n"
"suspended</a> cards."
msgstr ""
"\n"
"<h1>Glückwunsch!</h1>Du hast momentan alle Karten bearbeitet.<br><br>\n"
"%(next)s\n"
"<br><br>\n"
"Es gibt <b>%(waiting)d</b>\n"
"<a href=\"http://ichi2.net/anki/wiki/Key_Terms_and_Concepts#head-59a81e35b6afb23930005e943068945214d194b3\">\n"
"wartende</a> Karten.<br>\n"
"Es gibt <b>%(suspended)d</b>\n"
"<a href=\"http://ichi2.net/anki/wiki/Key_Terms_and_Concepts#head-37d2db274e6caa23aef55e29655a6b806901774b\">\n"
"ausgesetzte</a> Karten."
#: stdmodels.py:121
msgid ""
"\n"
"A format suitable for Heisig's \"Remembering the Kanji\".\n"
"You are tested from the keyword to the kanji.\n"
"\n"
"Layout of the test is based on the great work at\n"
"http://kanji.koohii.com/\n"
"\n"
"The link in the question will list user-contributed\n"
"stories. A free login is required."
msgstr ""
"\n"
"Ein Format welches sich zum Lernen von Heisig's \"Remembering the Kanji\" eignet.\n"
"Du musst das Kanji zum Schlüsselwort antworten.\n"
"\n"
"Das Format des Tests basiert auf\n"
"http://kanji.koohii.com/\n"
"\n"
"Der Hyperlink in den Fragen listet von Benutzern hinzugefügte Beispiele auf.\n"
"Ein kostenloser Login ist dafür notwendig."
#: stdmodels.py:101
msgid ""
"\n"
"Enter the English expression you want to learn in the 'Expression' field.\n"
"Enter a description in Japanese or English in the 'Meaning' field."
msgstr ""
"\n"
"Gib den englischen Ausdruck, den Du lernen möchtest, im Feld 'Ausdruck' ein.\n"
"Gib im Feld 'Bedeutung' eine Beschreibung in Japanisch oder Englisch ein."
#: stdmodels.py:52
msgid ""
"\n"
"The reading field is automatically generated by default,\n"
"and shows the reading for the expression. For words that\n"
"are normally written in hiragana or katakana and don't\n"
"need a reading, you can put the word in the expression\n"
"field, and leave the reading field blank. A reading will\n"
"will not automatically be generated for words written\n"
"in only hiragana or katakana.\n"
"\n"
"Note that the automatic generation of meaning is not\n"
"perfect, and should be checked before adding cards."
msgstr ""
#: stats.py:500
#, python-format
msgid "%(count)s %(gradename)s kanji."
msgstr "%(count)s %(gradename)s Kanji."
#: stats.py:498
#, python-format
msgid "%(gradename)s: %(count)s of %(total)s (%(percent)0.1f%%)."
msgstr "%(gradename)s: %(count)s von %(total)s (%(percent)0.1f%%)."
#: stats.py:277 stats.py:279
#, python-format
msgid "%0.1f seconds"
msgstr "%0.1f Sekunden"
#: stats.py:262 stats.py:303
#, python-format
msgid "%s ago"
msgstr "vor %s"
#: utils.py:19
#, python-format
msgid "%s day"
msgid_plural "%s days"
msgstr[0] "%s Tag"
msgstr[1] "%s Tage"
#: utils.py:20
#, python-format
msgid "%s hour"
msgid_plural "%s hours"
msgstr[0] "%s Stunde"
msgstr[1] "%s Stunden"
#: utils.py:21
#, python-format
msgid "%s minute"
msgid_plural "%s minutes"
msgstr[0] "%s Minute"
msgstr[1] "%s Minuten"
#: utils.py:18
#, python-format
msgid "%s month"
msgid_plural "%s months"
msgstr[0] "%s Monat"
msgstr[1] "%s Monate"
#: utils.py:22
#, python-format
msgid "%s second"
msgid_plural "%s seconds"
msgstr[0] "%s Sekunde"
msgstr[1] "%s Sekunden"
#: utils.py:17
#, python-format
msgid "%s year"
msgid_plural "%s years"
msgstr[0] "%s Jahr"
msgstr[1] "%s Jahre"
#: stats.py:370 stats.py:373 stats.py:376 stats.py:379
#, python-format
msgid "<b>%(a)d</b> (<b>%(b)0.1f</b>/day)"
msgstr "<b>%(a)d</b> (<b>%(b)0.1f</b>/Tag)"
#: stats.py:367
#, python-format
msgid "<b>%(a)d</b>/day, <b>%(b)d</b>/mon"
msgstr "<b>%(a)d</b>/Tag, <b>%(b)d</b>/Monat"
#: stats.py:356
#, python-format
msgid "<b>%0.0f</b> days"
msgstr "<b>%0.0f</b> Tage"
#: stats.py:357 stats.py:359 stats.py:361 stats.py:363 stats.py:365
#, python-format
msgid "<b>%0.1f</b> cards/day"
msgstr "<b>%0.1f</b> Karten/Tag"
#: stats.py:354
msgid "<b>Averages</b><br>"
msgstr "<b>Durchschnittswerte</b><br>"
#: stats.py:333
msgid "<b>Card counts</b><br>"
msgstr "<b>Anzahl Karten</b><br>"
#: stats.py:341
msgid "<b>Correct answers</b><br>"
msgstr "<b>Richtige Antworten</b><br>"
#: stats.py:522
#, python-format
msgid "<h1>Kanji statistics</h1>The %d seen cards in this deck contain:"
msgstr "<h1>Kanji-Statistik</h1>Die %d gesehenen Karten in diesem Stapel enthalten:"
#: stats.py:526
#, python-format
msgid "<li>%d total unique kanji.</li>"
msgstr "<li>insgesamt %d verschiedene Kanji.</li>"
#: sync.py:171
#, python-format
msgid ""
"<table width=500>\n"
"<tr><td><b>Added/Changed</b></td><td><b>Here</b></td><td><b>Server</b></td></tr>\n"
"<tr><td>Cards</td><td>%(lc)d</td><td>%(rc)d</td></tr>\n"
"<tr><td>Facts</td><td>%(lf)d</td><td>%(rf)d</td></tr>\n"
"<tr><td>Models</td><td>%(lm)d</td><td>%(rm)d</td></tr>\n"
"%(media)s\n"
"</table>"
msgstr ""
"<table width=500>\n"
"<tr><td><b>Hinzugefügt/Geändert</b></td><td><b>Lokal</b></td><td><b>Auf Server</b></td></tr>\n"
"<tr><td>Karten</td><td>%(lc)d</td><td>%(rc)d</td></tr>\n"
"<tr><td>Fakten</td><td>%(lf)d</td><td>%(rf)d</td></tr>\n"
"<tr><td>Modelle</td><td>%(lm)d</td><td>%(rm)d</td></tr>\n"
"%(media)s\n"
"</table>"
#: stdmodels.py:32
msgid ""
"A basic flashcard with a front and a back.\n"
"Questions are asked from front to back by default.\n"
"\n"
"Please consider customizing this model, rather than\n"
"using it verbatim: field names like \"expression\" are\n"
"clearer than \"front\" and \"back\", and will ensure\n"
"that your entries are consistent."
msgstr ""
"Eine normale Karteikarte mit Vorderseite und Rückseite.\n"
"Fragen werden von der Vorderseite zur Rückseite gestellt.\n"
"\n"
"Passe dieses Modell nach Möglichkeit für Deine Zwecke an.\n"
"Feldnamen wie \"Aussprache\", \"Deutsch\" etc. sind\n"
"verständlicher als \"Vorderseite\" und \"Rückseite\". Das gewährleistet außerdem,\n"
"dass Deine neu erstellten Karten konsistent sind."
#: stdmodels.py:164
msgid "A description in your native language, or Cantonese"
msgstr "Eine Beschreibung in Deiner eigenen Sprache oder auf Kantonesisch"
#: stdmodels.py:75
msgid "A description in your native language, or Japanese"
msgstr "Eine Beschreibung in Deiner eigenen Sprache oder auf Japanisch"
#: stdmodels.py:192
msgid "A description in your native language, or Mandarin"
msgstr "Eine Beschreibung in Deiner eigenen Sprache oder auf Mandarin"
#: stdmodels.py:38
msgid "A question."
msgstr "Eine Frage."
#: stdmodels.py:159 stdmodels.py:186
msgid "A word or expression written in Hanzi."
msgstr "Ein Wort oder Ausdruck in Hanzi."
#: stdmodels.py:65
msgid "A word or expression written in Kanji."
msgstr "Ein Wort oder Ausdruck in Kanji."
#: stdmodels.py:84 stdmodels.py:170 stdmodels.py:199
msgid "Actively test your recall by producing the target expression"
msgstr "Teste Deine Erinnerung, indem Du den Ausdruck hervorbringst"
#: stats.py:256
msgid "Added"
msgstr "Hinzugefügt"
#: stats.py:373
msgid "Added last month"
msgstr "Letzten Monat hinzugefügt"
#: stats.py:370
msgid "Added last week"
msgstr "Letzte Woche hinzugefügt"
#: exporting.py:216
msgid "Anki deck (*.anki)"
msgstr "Anki Stapeldatei (*.anki)"
#: exporting.py:55
msgid "Anki decks (*.anki)"
msgstr "Anki Stapeldateien (*.anki)"
#: deck.py:418
#, python-format
msgid ""
"At the same time tomorrow:<br><br>\n"
"- There will be <b>%(wait)d</b> cards waiting for review<br>\n"
"- There will be <b>%(new)d</b>\n"
"<a href=\"http://ichi2.net/anki/wiki/Key_Terms_and_Concepts#head-061e5433d4571d7ec7ecba0c329c09bd27c84d63\">\n"
"new cards</a> waiting"
msgstr ""
"Morgen werden zur gleichen Zeit <br><br>\n"
"- <b>%(wait)d</b> Karten zur Wiederholung bereit sein<br>\n"
"- <b>%(new)d</b>\n"
"<a href=\"http://ichi2.net/anki/wiki/Key_Terms_and_Concepts#head-061e5433d4571d7ec7ecba0c329c09bd27c84d63\">\n"
"neue Karten</a> verfügbar sein"
#: stdmodels.py:78 stdmodels.py:166 stdmodels.py:194
msgid "Automatically generated by default."
msgstr "Standardmäßig automatisch generiert."
#: stats.py:357
msgid "Average reps"
msgstr "Durchschnittliche Wiederholungen"
#: stats.py:277
msgid "Average time"
msgstr "Durchschnittliche Zeit"
#: stats.py:367
msgid "Avg. added"
msgstr "Durchschnittlich hinzugefügt"
#: stdmodels.py:42
msgid "Back to front"
msgstr "Rückseite zu Vorderseite"
#: stdmodels.py:31
msgid "Basic"
msgstr "Einfach"
#: deck.py:1452 deck.py:1525
msgid "Can't read/write deck"
msgstr "Kann den Stapel nicht lesen/schreiben"
#: stdmodels.py:156
msgid "Cantonese"
msgstr "Kantonesisch"
#: stats.py:290
msgid "Card model tags"
msgstr "Kartenmodell-Tags"
#: stats.py:289
msgid "Card tags"
msgstr "Karten-Tags"
#: graphs.py:77 graphs.py:99 graphs.py:113 graphs.py:135
msgid "Cards"
msgstr "Karten"
#: exporting.py:217
msgid "Cards in tab-separated text file (*.txt)"
msgstr "Karten in Tabulator-getrennter Textdatei (*.txt)"
#: models.py:25
msgid "Center"
msgstr "Zentriert"
#: stats.py:259
msgid "Changed"
msgstr "Geändert"
#: stats.py:274
msgid "Correct count"
msgstr "Anzahl richtiger Antworten"
#: stats.py:270
msgid "Current factor"
msgstr "Aktueller Faktor"
#: stats.py:266
msgid "Current interval"
msgstr "Aktuelles Intervall"
#: deck.py:1311
msgid "Database file damaged. Restore from backup."
msgstr "Datenbank-Datei defekt. Bitte von einem Backup wiederherstellen."
#: graphs.py:136
msgid "Day"
msgstr "Tag"
#: graphs.py:78 graphs.py:100 graphs.py:114
msgid "Days"
msgstr "Tage"
#: stats.py:318
msgid "Deck Statistics"
msgstr "Stapelstatistik"
#: stats.py:319
#, python-format
msgid "Deck created: <b>%s</b> ago<br>"
msgstr "Alter des Stapels: <b>%s</b><br>"
#: deck.py:1319
msgid "Deck was missing a model"
msgstr "Im Stapel fehlte ein Modell"
#: deck.py:1340
#, python-format
msgid "Deleted %d cards with missing fact"
msgstr "%d Karten mit fehlendem Faktum gelöscht"
#: deck.py:1348
#, python-format
msgid "Deleted %d cards with no card model"
msgstr "%d Karten ohne Modell gelöscht"
#: deck.py:1361
#, python-format
msgid "Deleted %d dangling fields"
msgstr "%d ungenutzte Felder gelöscht"
#: deck.py:1333
#, python-format
msgid "Deleted %d facts with missing fields"
msgstr "%d Fakten mit fehlenden Feldern gelöscht"
#: deck.py:1353
#, python-format
msgid "Deleted %d facts with no cards"
msgstr "%d Fakten ohne Karten gelöscht"
#: stdmodels.py:100
msgid "English"
msgstr "Englisch"
#: latex.py:104
msgid "Error executing 'latex' or 'dvipng' - are they installed?"
msgstr "Fehler beim Ausführen von 'latex' oder 'dvipng' - sind sie installiert?"
#: stats.py:294
msgid "Fact tags"
msgstr "Fakten-Tags"
#: exporting.py:218
msgid "Facts in tab-separated text file (*.txt)"
msgstr "Fakten in Tabulator-getrennter Textdatei (*.txt)"
#: deck.py:1497
msgid "File is in use by another process"
msgstr "Die Datei wird von einem anderen Prozess benutzt"
#: stats.py:379
msgid "First last month"
msgstr "Erstmalig letzten Monat"
#: stats.py:376
msgid "First last week"
msgstr "Erstmalig letzte Woche"
#: stats.py:258
msgid "First review"
msgstr "Erstmalig wiederholt"
#: stats.py:282
msgid "First time"
msgstr "Erstmalig"
#: stats.py:346
#, python-format
msgid "First-seen cards: <b>%(gNewYes%)0.1f%%</b> (<b>%(gNewYes)d</b> of <b>%(gNewTotal)d</b>)<br><br>"
msgstr "Erstmalig gesehene Karten: <b>%(gNewYes%)0.1f%%</b> (<b>%(gNewYes)d</b> von <b>%(gNewTotal)d</b>)<br><br>"
#: stdmodels.py:110
msgid "From the English expression to the meaning."
msgstr "Vom englischen Ausdruck zur Bedeutung."
#: stdmodels.py:142
msgid "From the keyword to the Kanji."
msgstr "Vom Schlüsselwort zum Kanji."
#: stdmodels.py:107
msgid "From the meaning to the English expression."
msgstr "Von der Bedeutung zum englischen Ausdruck."
#: stdmodels.py:40
msgid "Front to back"
msgstr "Vorderseite zu Rückseite"
#: stdmodels.py:120
msgid "Heisig"
msgstr "Heisig"
#: stats.py:356
msgid "Interval"
msgstr "Intervall"
#: stdmodels.py:51
msgid "Japanese"
msgstr "Japanisch"
#: stats.py:537
msgid "Jouyou levels:"
msgstr "Jouyou-Level:"
#: stats.py:271
msgid "Last factor"
msgstr "Vorheriger Faktor"
#: stats.py:269
msgid "Last interval"
msgstr "Vorheriges Intervall"
#: models.py:26
msgid "Left"
msgstr "Links"
#: stdmodels.py:183
msgid "Mandarin"
msgstr "Mandarin"
#: stats.py:286
msgid "Mature"
msgstr "Alt"
#: graphs.py:210
msgid "Mature cards"
msgstr "Alte Karten"
#: stats.py:342
#, python-format
msgid "Mature cards: <b>%(gMatureYes%)0.1f%%</b> (<b>%(gMatureYes)d</b> of <b>%(gMatureTotal)d</b>)<br>"
msgstr "Alte Karten: <b>%(gMatureYes%)0.1f%%</b> (<b>%(gMatureYes)d</b> von <b>%(gMatureTotal)d</b>)<br>"
#: stats.py:334
#, python-format
msgid "Mature cards: <b>%(old)d</b> (<b>%(oldP)0.2f%%</b>)<br>"
msgstr "Alte Karten: <b>%(old)d</b> (<b>%(oldP)0.2f%%</b>)<br>"
#: media.py:161 media.py:163 media.py:164
msgid "Media Missing"
msgstr "Fehlende Mediendateien"
#: deck.py:915
#, python-format
msgid "Model '%s' has wrong card model count"
msgstr "Modell '%s' hat eine falsche Anzahl Karten"
#: deck.py:919
#, python-format
msgid "Model '%s' has wrong field model count"
msgstr "Modell '%s' hat eine falsche Anzahl Felder"
#: stats.py:292
msgid "Model tags"
msgstr "Modell-Tags"
#: graphs.py:208
msgid "New cards"
msgstr "Neue Karten"
#: stats.py:265
msgid "Next due"
msgstr "Wieder fällig"
#: stats.py:316
msgid "Please add some cards first.<p/>"
msgstr "Bitte füge zuerst ein paar Karten hinzu.<p/>"
#: stats.py:276
msgid "Repeatedly correct"
msgstr "Wiederholt richtig"
#: stats.py:365
msgid "Reps last month"
msgstr "Wiederholungen letzten Monat"
#: stats.py:363
msgid "Reps last week"
msgstr "Wiederholungen letzte Woche"
#: stats.py:361
msgid "Reps next month"
msgstr "Wiederholungen nächsten Monat"
#: stats.py:359
msgid "Reps next week"
msgstr "Wiederholungen nächste Woche"
#: stats.py:272
msgid "Review count"
msgstr "Anzahl Wiederholungen"
#: models.py:27
msgid "Right"
msgstr "Rechts"
#: deck.py:1824
msgid "Show new cards after all other cards"
msgstr "Zeige neue Karten nach allen anderen Karten"
#: deck.py:1818
msgid "Show new cards in order they were added"
msgstr "Zeige neue Karten in der Reihenfolge des Hinzufügens"
#: deck.py:1817
msgid "Show new cards in random order"
msgstr "Zeige neue Karten in zufälliger Reihenfolge"
#: deck.py:1823
msgid "Spread new cards out through reviews"
msgstr "Neue Karten über die Wiederholungen verteilen"
#: stats.py:287
msgid "State"
msgstr "Zustand"
#: stdmodels.py:88 stdmodels.py:174 stdmodels.py:203
msgid "Test your ability to recognize the target expression"
msgstr "Teste Deine Fähigkeit, den Ausdruck zu erkennen"
#: exporting.py:145 exporting.py:177
msgid "Text files (*.txt)"
msgstr "Textdateien (*.txt)"
#: stdmodels.py:39
msgid "The answer."
msgstr "Die Antwort."
#: deck.py:1325
msgid "The current model didn't exist"
msgstr "Das aktuelle Modell war nicht vorhanden"
#: deck.py:431
msgid "The deck is empty. Please add some cards."
msgstr "Der Stapel ist leer. Bitte füge ein paar Karten hinzu."
#: deck.py:428
#, python-format
msgid "The next card will be shown in <b>%s</b>"
msgstr "Die nächste Karte wird in <b>%s</b> gezeigt"
#: stats.py:331
#, python-format
msgid "Total number of cards: <b>%d</b><br><br>"
msgstr "Gesamtanzahl Karten: <b>%d</b><br><br>"
#: stats.py:279
msgid "Total time"
msgstr "Gesamtzeit"
#: deck.py:702
msgid "Unknown"
msgstr "Unbekannt"
#: stats.py:338
#, python-format
msgid "Unseen cards: <b>%(new)d</b> (<b>%(newP)0.2f%%</b>)<br><br>"
msgstr "Nicht gesehene Karten: <b>%(new)d</b> (<b>%(newP)0.2f%%</b>)<br><br>"
#: stats.py:284
msgid "Young"
msgstr "Jung"
#: graphs.py:209
msgid "Young cards"
msgstr "Junge Karten"
#: stats.py:344
#, python-format
msgid "Young cards: <b>%(gYoungYes%)0.1f%%</b> (<b>%(gYoungYes)d</b> of <b>%(gYoungTotal)d</b>)<br>"
msgstr "Junge Karten: <b>%(gYoungYes%)0.1f%%</b> (<b>%(gYoungYes)d</b> von <b>%(gYoungTotal)d</b>)<br>"
#: stats.py:336
#, python-format
msgid "Young cards: <b>%(young)d</b> (<b>%(youngP)0.2f%%</b>)<br>"
msgstr "Junge Karten: <b>%(young)d</b> (<b>%(youngP)0.2f%%</b>)<br>"
#: models.py:150 models.py:151 models.py:182
msgid "[empty]"
msgstr "[leer]"
#: models.py:148
msgid "[invalid format; see model properties]"
msgstr "[ungültiges Format; siehe Modelleigenschaften]"
#: deck.py:463
msgid "a short time"
msgstr "eine kurze Zeit"
#: stats.py:264
#, python-format
msgid "in %s"
msgstr "in %s"
#: deck.py:448
msgid "unknown"
msgstr "unbekannt"

View file

@ -0,0 +1,815 @@
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: Anki 0.9.7.7\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2008-09-23 03:21+0900\n"
"PO-Revision-Date: 2008-09-03 02:48+0100\n"
"Last-Translator: Pcsl <pcsl88@gmail.com>\n"
"Language-Team: Spanish <pcsl88@gmail.com>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Poedit-Language: Spanish\n"
"X-Poedit-Country: SPAIN\n"
#: deck.py:483
#, fuzzy, python-format
msgid ""
"\n"
"<h1>Congratulations!</h1>You have finished the deck for now.<br><br>\n"
"%(next)s\n"
"<br><br>\n"
"- There are <b>%(waiting)d</b>\n"
"<a href=\"http://ichi2.net/anki/wiki/Key_Terms_and_Concepts#head-59a81e35b6afb23930005e943068945214d194b3\">\n"
"spaced</a> cards.<br>\n"
"- There are <b>%(suspended)d</b>\n"
"<a href=\"http://ichi2.net/anki/wiki/Key_Terms_and_Concepts#head-37d2db274e6caa23aef55e29655a6b806901774b\">\n"
"suspended</a> cards."
msgstr ""
"\n"
"<h1>¡Felicidades!</h1>Ha finalizado el mazo por ahora.<br><br>\n"
"%(next)s\n"
"<br><br>\n"
"Hay %(waiting)d\n"
"\n"
"tarjetas <a href=\"http://ichi2.net/anki/wiki/Key_Terms_and_Concepts#head-59a81e35b6afb23930005e943068945214d194b3\">espaciadas</a>.<br>\n"
"Hay %(suspended)d\n"
"\n"
"tarjetas <a href=\"http://ichi2.net/anki/wiki/Key_Terms_and_Concepts#head-37d2db274e6caa23aef55e29655a6b806901774b\">suspendidas</a>."
#: stdmodels.py:121
msgid ""
"\n"
"A format suitable for Heisig's \"Remembering the Kanji\".\n"
"You are tested from the keyword to the kanji.\n"
"\n"
"Layout of the test is based on the great work at\n"
"http://kanji.koohii.com/\n"
"\n"
"The link in the question will list user-contributed\n"
"stories. A free login is required."
msgstr ""
"\n"
"Un formato idóneo para el libro \"Kanji para Recordar\" de Heisig.\n"
"Será examinado desde la palabra clave a los kanji.\n"
"\n"
"La plantilla está basada en el gran trabajo en\n"
"http://kanji.koohii.com/\n"
"\n"
"El enlace de la pregunta le mostrará historias hechas\n"
"por los usuarios. Se requiere una identificación gratuita."
#: stdmodels.py:101
msgid ""
"\n"
"Enter the English expression you want to learn in the 'Expression' field.\n"
"Enter a description in Japanese or English in the 'Meaning' field."
msgstr ""
"\n"
"Introduzca la expresión en inglés que desea aprender en el campo 'Expresión.\n"
"Introduzca una descripción en japonés o inglés en el campo de 'Significado'."
#: stdmodels.py:52
msgid ""
"\n"
"The reading field is automatically generated by default,\n"
"and shows the reading for the expression. For words that\n"
"are normally written in hiragana or katakana and don't\n"
"need a reading, you can put the word in the expression\n"
"field, and leave the reading field blank. A reading will\n"
"will not automatically be generated for words written\n"
"in only hiragana or katakana.\n"
"\n"
"Note that the automatic generation of meaning is not\n"
"perfect, and should be checked before adding cards."
msgstr ""
"\n"
"El campo de lectura se genera automáticamente por defecto,\n"
"y muestra la lectura de la expresión. Para las palabras que\n"
"se escriben normalmente en hiragana y katakana y no necesitan\n"
"una lectura, puede poner la palabra en el campo de expresión,\n"
"y dejar el campo de lectura en blanco. La lectura no\n"
"será creada automáticamente para palabras escritas\n"
"solamente en hiragana o katakana.\n"
"\n"
"Dése cuenta de que la creación automática de significados no es\n"
"perfecta, y debe ser comprobado antes de añadir significados."
#: stats.py:500
#, python-format
msgid "%(count)s %(gradename)s kanji."
msgstr "%(count)s %(gradename)s kanji.s"
#: stats.py:498
#, python-format
msgid "%(gradename)s: %(count)s of %(total)s (%(percent)0.1f%%)."
msgstr "%(gradename)s: %(count)s de %(total)s (%(percent)0.1f%%)."
#: stats.py:277 stats.py:279
#, python-format
msgid "%0.1f seconds"
msgstr "%0.1f segundos"
#: stats.py:262 stats.py:303
#, python-format
msgid "%s ago"
msgstr "hace %s"
#: utils.py:19
#, python-format
msgid "%s day"
msgid_plural "%s days"
msgstr[0] "%s día"
msgstr[1] "%s días"
#: utils.py:20
#, python-format
msgid "%s hour"
msgid_plural "%s hours"
msgstr[0] "%s hora"
msgstr[1] "%s horas"
#: utils.py:21
#, python-format
msgid "%s minute"
msgid_plural "%s minutes"
msgstr[0] "%s minuto"
msgstr[1] "%s minutos"
#: utils.py:18
#, python-format
msgid "%s month"
msgid_plural "%s months"
msgstr[0] "%s mes"
msgstr[1] "%s meses"
#: utils.py:22
#, python-format
msgid "%s second"
msgid_plural "%s seconds"
msgstr[0] "%s segundo"
msgstr[1] "%s segundos"
#: utils.py:17
#, python-format
msgid "%s year"
msgid_plural "%s years"
msgstr[0] "%s año"
msgstr[1] "%s años"
#: stats.py:370 stats.py:373 stats.py:376 stats.py:379
#, fuzzy, python-format
msgid "<b>%(a)d</b> (<b>%(b)0.1f</b>/day)"
msgstr "Tarjetas maduras: <b>%(old)d</b> (<b>%(oldP)0.2f%%</b>)<br>"
#: stats.py:367
#, fuzzy, python-format
msgid "<b>%(a)d</b>/day, <b>%(b)d</b>/mon"
msgstr "Añadidas: <b>%(a)d</b> por día, <b>%(b)d</b> por mes<br>"
#: stats.py:356
#, fuzzy, python-format
msgid "<b>%0.0f</b> days"
msgstr "Intervalo: <b>%0.0f</b> días<br/>"
#: stats.py:357 stats.py:359 stats.py:361 stats.py:363 stats.py:365
#, fuzzy, python-format
msgid "<b>%0.1f</b> cards/day"
msgstr "Volumen de trabajo: <b>%0.1f</b> tarjetas/día<br/>"
#: stats.py:354
msgid "<b>Averages</b><br>"
msgstr "<b>Medias</b><br>"
#: stats.py:333
msgid "<b>Card counts</b><br>"
msgstr "<b>Número de tarjetas</b><br>"
#: stats.py:341
msgid "<b>Correct answers</b><br>"
msgstr "<b>Respuestas correctas</b><br>"
#: stats.py:522
#, python-format
msgid "<h1>Kanji statistics</h1>The %d seen cards in this deck contain:"
msgstr "<h1>Estadísticas de kanjis</h1>Las %d tarjetas vistas en este mazo contienen:"
#: stats.py:526
#, python-format
msgid "<li>%d total unique kanji.</li>"
msgstr "<li>%d kanjis únicos totales.</li>"
#: sync.py:171
#, python-format
msgid ""
"<table width=500>\n"
"<tr><td><b>Added/Changed</b></td><td><b>Here</b></td><td><b>Server</b></td></tr>\n"
"<tr><td>Cards</td><td>%(lc)d</td><td>%(rc)d</td></tr>\n"
"<tr><td>Facts</td><td>%(lf)d</td><td>%(rf)d</td></tr>\n"
"<tr><td>Models</td><td>%(lm)d</td><td>%(rm)d</td></tr>\n"
"%(media)s\n"
"</table>"
msgstr ""
#: stdmodels.py:32
msgid ""
"A basic flashcard with a front and a back.\n"
"Questions are asked from front to back by default.\n"
"\n"
"Please consider customizing this model, rather than\n"
"using it verbatim: field names like \"expression\" are\n"
"clearer than \"front\" and \"back\", and will ensure\n"
"that your entries are consistent."
msgstr ""
"Una tarjeta flash básica con la cara y el reverso.\n"
"Las preguntas serán examinadas por defecto, desde la cara al reverso .\n"
"\n"
"Tenga en cuenta que puede personalizar el modelo, en lugar de\n"
"dejarlo tal cual: campos como \"expresión\" son\n"
"más clarificadores que \"cara\" y \"reverso\", y se asegurará de\n"
"que sus entradas sean más consecuentes."
#: stdmodels.py:164
msgid "A description in your native language, or Cantonese"
msgstr "Una descripición en su lengua materna, o cantonés"
#: stdmodels.py:75
msgid "A description in your native language, or Japanese"
msgstr "Una descripición en su lengua materna, o japonés"
#: stdmodels.py:192
msgid "A description in your native language, or Mandarin"
msgstr "Una descripición en su lengua materna, o mMandarín"
#: stdmodels.py:38
msgid "A question."
msgstr "Una pregunta."
#: stdmodels.py:159 stdmodels.py:186
msgid "A word or expression written in Hanzi."
msgstr "Una palabra o expresión escrita en Hanzi."
#: stdmodels.py:65
msgid "A word or expression written in Kanji."
msgstr "Una palabra o expresión escrita en Kanji."
#: stdmodels.py:84 stdmodels.py:170 stdmodels.py:199
msgid "Actively test your recall by producing the target expression"
msgstr "Comprueba su memoria activamente escribiendo la expresión de destino"
#: stats.py:256
msgid "Added"
msgstr "Añadido"
#: stats.py:373
msgid "Added last month"
msgstr ""
#: stats.py:370
msgid "Added last week"
msgstr ""
#: exporting.py:216
msgid "Anki deck (*.anki)"
msgstr "Mazo Anki (*.anki)"
#: exporting.py:55
msgid "Anki decks (*.anki)"
msgstr "Mazos Anki (*.anki)"
#: deck.py:418
#, python-format
msgid ""
"At the same time tomorrow:<br><br>\n"
"- There will be <b>%(wait)d</b> cards waiting for review<br>\n"
"- There will be <b>%(new)d</b>\n"
"<a href=\"http://ichi2.net/anki/wiki/Key_Terms_and_Concepts#head-061e5433d4571d7ec7ecba0c329c09bd27c84d63\">\n"
"new cards</a> waiting"
msgstr ""
#: stdmodels.py:78 stdmodels.py:166 stdmodels.py:194
msgid "Automatically generated by default."
msgstr "Creado automáticamente por defecto."
#: stats.py:357
#, fuzzy
msgid "Average reps"
msgstr "Tiempo medio"
#: stats.py:277
msgid "Average time"
msgstr "Tiempo medio"
#: stats.py:367
#, fuzzy
msgid "Avg. added"
msgstr "Añadido"
#: stdmodels.py:42
msgid "Back to front"
msgstr "Desde el reverso a la cara"
#: stdmodels.py:31
msgid "Basic"
msgstr "Básico"
#: deck.py:1452 deck.py:1525
msgid "Can't read/write deck"
msgstr "No se puede leer/escribir el mazo"
#: stdmodels.py:156
msgid "Cantonese"
msgstr "Cantonés"
#: stats.py:290
msgid "Card model tags"
msgstr "Etiquetas del modelo de tarjetas"
#: stats.py:289
msgid "Card tags"
msgstr "Etiquetas de la tarjeta"
#: graphs.py:77 graphs.py:99 graphs.py:113 graphs.py:135
msgid "Cards"
msgstr "Tarjetas"
#: exporting.py:217
msgid "Cards in tab-separated text file (*.txt)"
msgstr "Tarjetas en un archivo de texto separado por tabulador (*.txt)"
#: models.py:25
msgid "Center"
msgstr "Centrar"
#: stats.py:259
msgid "Changed"
msgstr "Cambiado"
#: stats.py:274
msgid "Correct count"
msgstr "Veces correcto"
#: stats.py:270
msgid "Current factor"
msgstr "Factor actual"
#: stats.py:266
msgid "Current interval"
msgstr "Intervalo actual"
#: deck.py:1311
msgid "Database file damaged. Restore from backup."
msgstr "Archivo de Base de Datos dañado. Restaurela de la copia de seguridad."
#: graphs.py:136
msgid "Day"
msgstr ""
#: graphs.py:78 graphs.py:100 graphs.py:114
msgid "Days"
msgstr ""
#: stats.py:318
msgid "Deck Statistics"
msgstr "Estadísticas del Mazo"
#: stats.py:319
#, python-format
msgid "Deck created: <b>%s</b> ago<br>"
msgstr "Mazo creado: Hace <b>%s</b><br>"
#: deck.py:1319
msgid "Deck was missing a model"
msgstr "El mazo carecía de un modelo."
#: deck.py:1340
#, python-format
msgid "Deleted %d cards with missing fact"
msgstr "Borradas %d tarjetas que carecían de hecho"
#: deck.py:1348
#, python-format
msgid "Deleted %d cards with no card model"
msgstr "Borradas %d tarjetas que carecían de modelo de tarjeta"
#: deck.py:1361
#, python-format
msgid "Deleted %d dangling fields"
msgstr "Borrados %d campos pendientes"
#: deck.py:1333
#, python-format
msgid "Deleted %d facts with missing fields"
msgstr "Borrados %d hechos con campos faltantes"
#: deck.py:1353
#, python-format
msgid "Deleted %d facts with no cards"
msgstr "Borrados %d hechos sin tarjetas"
#: stdmodels.py:100
msgid "English"
msgstr "Inglés"
#: latex.py:104
msgid "Error executing 'latex' or 'dvipng' - are they installed?"
msgstr "Error ejecutando 'latex' o 'dvipng' - ¿Están instalados?"
#: stats.py:294
msgid "Fact tags"
msgstr "Etiquetas de hechos"
#: exporting.py:218
msgid "Facts in tab-separated text file (*.txt)"
msgstr "Hechos en un archivo de texto separado por tabulador (*.txt)"
#: deck.py:1497
msgid "File is in use by another process"
msgstr "El archivo está en uso por otro proceso"
#: stats.py:379
#, fuzzy
msgid "First last month"
msgstr "Primera vez"
#: stats.py:376
#, fuzzy
msgid "First last week"
msgstr "Primera vez"
#: stats.py:258
msgid "First review"
msgstr "Primer repaso"
#: stats.py:282
msgid "First time"
msgstr "Primera vez"
#: stats.py:346
#, python-format
msgid "First-seen cards: <b>%(gNewYes%)0.1f%%</b> (<b>%(gNewYes)d</b> of <b>%(gNewTotal)d</b>)<br><br>"
msgstr "Vistas por primera vez: <b>%(gNewYes%)0.1f%%</b> (<b>%(gNewYes)d</b> de <b>%(gNewTotal)d</b>)<br><br>"
#: stdmodels.py:110
msgid "From the English expression to the meaning."
msgstr "Desde la expresión en inglés al significado."
#: stdmodels.py:142
msgid "From the keyword to the Kanji."
msgstr "Desde la palabra clave a el Kanji."
#: stdmodels.py:107
msgid "From the meaning to the English expression."
msgstr "Desde el significado a la expresión en inglés."
#: stdmodels.py:40
msgid "Front to back"
msgstr "Desde la cara al reverso"
#: stdmodels.py:120
msgid "Heisig"
msgstr "Heisig"
#: stats.py:356
#, fuzzy
msgid "Interval"
msgstr "Último intervalo"
#: stdmodels.py:51
msgid "Japanese"
msgstr "Japonés"
#: stats.py:537
msgid "Jouyou levels:"
msgstr "Niveles Jouyou:"
#: stats.py:271
msgid "Last factor"
msgstr "Último factor"
#: stats.py:269
msgid "Last interval"
msgstr "Último intervalo"
#: models.py:26
msgid "Left"
msgstr "Izquierda"
#: stdmodels.py:183
msgid "Mandarin"
msgstr "Mandarín"
#: stats.py:286
msgid "Mature"
msgstr "Madura"
#: graphs.py:210
msgid "Mature cards"
msgstr "Tarjetas maduras"
#: stats.py:342
#, python-format
msgid "Mature cards: <b>%(gMatureYes%)0.1f%%</b> (<b>%(gMatureYes)d</b> of <b>%(gMatureTotal)d</b>)<br>"
msgstr "Tarjetas maduras: <b>%(gMatureYes%)0.1f%%</b> (<b>%(gMatureYes)d</b> de <b>%(gMatureTotal)d</b>)<br>"
#: stats.py:334
#, python-format
msgid "Mature cards: <b>%(old)d</b> (<b>%(oldP)0.2f%%</b>)<br>"
msgstr "Tarjetas maduras: <b>%(old)d</b> (<b>%(oldP)0.2f%%</b>)<br>"
#: media.py:161 media.py:163 media.py:164
msgid "Media Missing"
msgstr ""
#: deck.py:915
#, python-format
msgid "Model '%s' has wrong card model count"
msgstr "El modelo '%s' tenía un número incorrecto de modelos de tarjeta"
#: deck.py:919
#, python-format
msgid "Model '%s' has wrong field model count"
msgstr "El modelo '%s' tenía un número incorrecto de modelos de campo"
#: stats.py:292
msgid "Model tags"
msgstr "Etiquetas del modelo"
#: graphs.py:208
msgid "New cards"
msgstr "Nueva tarjeta"
#: stats.py:265
msgid "Next due"
msgstr "Siguiente repaso dentro de"
#: stats.py:316
msgid "Please add some cards first.<p/>"
msgstr "Por favor, añada algunas tarjetas antes.<p/>"
#: stats.py:276
msgid "Repeatedly correct"
msgstr "Repetido correctamente"
#: stats.py:365
#, fuzzy
msgid "Reps last month"
msgstr "%s mes"
#: stats.py:363
msgid "Reps last week"
msgstr ""
#: stats.py:361
#, fuzzy
msgid "Reps next month"
msgstr "%s mes"
#: stats.py:359
msgid "Reps next week"
msgstr ""
#: stats.py:272
msgid "Review count"
msgstr "Veces repasado"
#: models.py:27
msgid "Right"
msgstr "Derecha"
#: deck.py:1824
#, fuzzy
msgid "Show new cards after all other cards"
msgstr "Mostrar nuevas tarjetas en el orden en el que fueron añadidas"
#: deck.py:1818
msgid "Show new cards in order they were added"
msgstr "Mostrar nuevas tarjetas en el orden en el que fueron añadidas"
#: deck.py:1817
msgid "Show new cards in random order"
msgstr "Mostrar nuevas tarjetas de forma aleatoria"
#: deck.py:1823
msgid "Spread new cards out through reviews"
msgstr ""
#: stats.py:287
msgid "State"
msgstr "Estado"
#: stdmodels.py:88 stdmodels.py:174 stdmodels.py:203
msgid "Test your ability to recognize the target expression"
msgstr "Compruebe su habilidad para reconocer la expresión de destino"
#: exporting.py:145 exporting.py:177
msgid "Text files (*.txt)"
msgstr "Archivos de texto (*.txt)"
#: stdmodels.py:39
msgid "The answer."
msgstr "La respuesta."
#: deck.py:1325
msgid "The current model didn't exist"
msgstr "El modelo actual no existía"
#: deck.py:431
msgid "The deck is empty. Please add some cards."
msgstr "El mazo está vacío. Por favor, añada algunas tarjetas."
#: deck.py:428
#, python-format
msgid "The next card will be shown in <b>%s</b>"
msgstr "La siguiente tarjeta se mostrará en <b>%s</b>"
#: stats.py:331
#, python-format
msgid "Total number of cards: <b>%d</b><br><br>"
msgstr "Número total de tarjetas: <b>%d</b><br><br>"
#: stats.py:279
msgid "Total time"
msgstr "Tiempo total"
#: deck.py:702
msgid "Unknown"
msgstr "Desconocido"
#: stats.py:338
#, python-format
msgid "Unseen cards: <b>%(new)d</b> (<b>%(newP)0.2f%%</b>)<br><br>"
msgstr "Tarjetas no mostradas: <b>%(new)d</b> (<b>%(newP)0.2f%%</b>)<br><br>"
#: stats.py:284
msgid "Young"
msgstr "Joven"
#: graphs.py:209
msgid "Young cards"
msgstr "Tarjetas jóvenes"
#: stats.py:344
#, python-format
msgid "Young cards: <b>%(gYoungYes%)0.1f%%</b> (<b>%(gYoungYes)d</b> of <b>%(gYoungTotal)d</b>)<br>"
msgstr "Tarjetas jóvenes: <b>%(gYoungYes%)0.1f%%</b> (<b>%(gYoungYes)d</b> de <b>%(gYoungTotal)d</b>)<br>"
#: stats.py:336
#, python-format
msgid "Young cards: <b>%(young)d</b> (<b>%(youngP)0.2f%%</b>)<br>"
msgstr "Tarjetas jóvenes: <b>%(young)d</b> (<b>%(youngP)0.2f%%</b>)<br>"
#: models.py:150 models.py:151 models.py:182
msgid "[empty]"
msgstr "[vacío]"
#: models.py:148
msgid "[invalid format; see model properties]"
msgstr "[formato no válido; vea las propiedades del modelo]"
#: deck.py:463
msgid "a short time"
msgstr "en poco tiempo"
#: stats.py:264
#, python-format
msgid "in %s"
msgstr "en %s"
#: deck.py:448
msgid "unknown"
msgstr "desconocido"
#~ msgid ""
#~ "\n"
#~ "Two cards: one tests meaning -> imperfective + perfective,\n"
#~ "and one tests imperfective -> perfective."
#~ msgstr ""
#~ "\n"
#~ "Dos tarjetas: una pregunta el significado -> imperfectivo + perfectivo,\n"
#~ "y la otra pregunta imperfectivo -> perfectivo."
#~ msgid ""
#~ "\n"
#~ "Two cards: one tests production, one recognition."
#~ msgstr ""
#~ "\n"
#~ "Dos tarjetas: una le examina activamente, la otra le examina en el reconocimiento."
#~ msgid "%(a)d.%(b)df day"
#~ msgstr "%(a)d.%(b)df días"
#~ msgid "%(a)d.%(b)df hour"
#~ msgstr "%(a)d.%(b)df horas"
#~ msgid "%(a)d.%(b)df minute"
#~ msgstr "%(a)d.%(b)df minutos"
#~ msgid "%(a)d.%(b)df second"
#~ msgstr "%(a)d.%(b)df segundos"
#~ msgid "%(a)d.%(b)df year"
#~ msgstr "%(a)d.%(b)df años"
#~ msgid "%s days"
#~ msgstr "%s días"
#~ msgid "%x at %H:%M"
#~ msgstr "%x a las %H:%M"
#~ msgid "A random position in the deck"
#~ msgstr "Posición aleatoria en el mazo"
#~ msgid "At the end of the deck"
#~ msgstr "Al final del mazo"
#~ msgid "At the same time tomorrow, there will be <b>%d</b> cards waiting"
#~ msgstr "A la misma hora mañana, habrá <b>%d</b> tarjetas que responder"
#~ msgid "At the start of the deck"
#~ msgstr "Al principio del mazo"
#~ msgid "Can't read/write directory"
#~ msgstr "No se puede leer/escribir el directorio"
#~ msgid "Colon/semicolon/tab-separated text file (*.*)"
#~ msgstr "Archivo de texto separado por Coma/punto y coma/tabulador (*.*)"
#~ msgid "Couldn't find pattern. The file should be a series of lines separated by colons, semicolons or tabs."
#~ msgstr "No se ajusta a la plantilla. El archivo debe estar compuesto por una serie de líneas separadas por punto, coma y punto o tabulador."
#~ msgid "Deck%"
#~ msgstr "Mazo%"
#~ msgid "Error executing 'latex' - is it installed?"
#~ msgstr "Error ejecutando 'latex' -¿Está instalado?"
#~ msgid "Fc .pending file (*.pending *.imported)"
#~ msgstr "Archivo Fc .pendiente (*.pending *.imported)"
#~ msgid "From imperfective to perfective"
#~ msgstr "De Imperfectivo a perfectivo"
#~ msgid "From the Russian expression to the meaning."
#~ msgstr "Desde la expresión en ruso al significado."
#~ msgid "From the meaning to an imperfective+perfective pair."
#~ msgstr "Desde el significado a el par imperfectivo+perfectivo."
#~ msgid "From the meaning to the Russian expression."
#~ msgstr "Desde el signficado a la expresión en ruso."
#~ msgid "Import: duplicate"
#~ msgstr "Importar: duplicado"
#~ msgid "Import: missing field"
#~ msgstr "Importar: campo no presente"
#~ msgid "Import: same %s"
#~ msgstr "Importar: mismo %s"
#~ msgid "Line %(line)d doesn't match pattern '%(pat)s'"
#~ msgstr "La línea %(line)d no se ajusta a la plantilla '%(pat)s'"
#~ msgid "Line %(line)d had %(num1)d fields, expected %(num2)d"
#~ msgstr "La línea %(line)d tiene %(num1)d campos, se esperaban %(num2)d"
#~ msgid "Mnemosyne file (*.mem)"
#~ msgstr "Archivo Mnemosyne (*.mem)"
#~ msgid "Model"
#~ msgstr "Modelo"
#~ msgid "Old Anki 0.2.x file (*.fc)"
#~ msgstr "Antiguo archivo Anki 0.2.x (*.fc)"
#~ msgid "Old cards"
#~ msgstr "Tarjetas viejas"
#~ msgid "Other"
#~ msgstr "Otro"
#~ msgid "Period"
#~ msgstr "Periodo"
#~ msgid "Russian"
#~ msgstr "Ruso"
#~ msgid "Russian verbs (imperfective + perfective)"
#~ msgstr "Verbos en ruso (imperfectivo+perfectivo)"
#~ msgid "The file had no non-empty lines."
#~ msgstr "El archivo no tenía líneas vacías."
#~ msgid "The file was not in UTF8 format."
#~ msgstr "El archivo no estaba en formato UTF8."
#~ msgid "Untitled"
#~ msgstr "Sin título"
#~ msgid "in %(a)s, on %(b)s"
#~ msgstr "en %(a)s, en %(b)s"

View file

@ -0,0 +1,813 @@
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2008-09-23 03:21+0900\n"
"PO-Revision-Date: 2008-09-18 01:16+0100\n"
"Last-Translator: Emmanuel JARRI <emmanuel.jarri@gmail.com>\n"
"Language-Team: LMS <anki_tradu@laurentsteffan.com>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Poedit-Language: French\n"
"X-Poedit-Country: FRANCE\n"
"X-Poedit-SourceCharset: utf-8\n"
"Plural-Forms: nplurals=2; plural=n>1;\n"
#: deck.py:483
#, python-format
msgid ""
"\n"
"<h1>Congratulations!</h1>You have finished the deck for now.<br><br>\n"
"%(next)s\n"
"<br><br>\n"
"- There are <b>%(waiting)d</b>\n"
"<a href=\"http://ichi2.net/anki/wiki/Key_Terms_and_Concepts#head-59a81e35b6afb23930005e943068945214d194b3\">\n"
"spaced</a> cards.<br>\n"
"- There are <b>%(suspended)d</b>\n"
"<a href=\"http://ichi2.net/anki/wiki/Key_Terms_and_Concepts#head-37d2db274e6caa23aef55e29655a6b806901774b\">\n"
"suspended</a> cards."
msgstr ""
"\n"
"<h1>Félicitations!</h1>Vous avez fini le paquet pour aujourd'hui.<br><br>\n"
"%(next)s\n"
"<br><br>\n"
"- Il y a <b>%(waiting)d</b>\n"
" cartes <a href=\"http://ichi2.net/anki/wiki/Key_Terms_and_Concepts#head-59a81e35b6afb23930005e943068945214d194b3\">\n"
"espacées</a>.<br>\n"
"- Il y a <b>%(suspended)d</b>\n"
" cartes <a href=\"http://ichi2.net/anki/wiki/Key_Terms_and_Concepts#head-37d2db274e6caa23aef55e29655a6b806901774b\">\n"
"suspendues</a>."
#: stdmodels.py:121
msgid ""
"\n"
"A format suitable for Heisig's \"Remembering the Kanji\".\n"
"You are tested from the keyword to the kanji.\n"
"\n"
"Layout of the test is based on the great work at\n"
"http://kanji.koohii.com/\n"
"\n"
"The link in the question will list user-contributed\n"
"stories. A free login is required."
msgstr ""
"\n"
"Un format adapté à \"Remembering the Kanji\" de Heisig.\n"
"Vous êtes testé dans le sens mot-clé vers kanji.\n"
"\n"
"La disposition du test se fonde sur l'excellent travail du site \n"
"http://kanji.koohii.com/\n"
"\n"
"Le lien dans la question donne la liste des histoires proposées par\n"
"les utilisateurs. Vous devez vous enregistrer, c'est gratuit."
#: stdmodels.py:101
msgid ""
"\n"
"Enter the English expression you want to learn in the 'Expression' field.\n"
"Enter a description in Japanese or English in the 'Meaning' field."
msgstr ""
"\n"
"Saisissez l'expression en français que vous souhaitez apprendre dans le champ 'Expression'.\n"
"Saisissez une description en japonais ou en français dans le champ 'Signification'."
#: stdmodels.py:52
msgid ""
"\n"
"The reading field is automatically generated by default,\n"
"and shows the reading for the expression. For words that\n"
"are normally written in hiragana or katakana and don't\n"
"need a reading, you can put the word in the expression\n"
"field, and leave the reading field blank. A reading will\n"
"will not automatically be generated for words written\n"
"in only hiragana or katakana.\n"
"\n"
"Note that the automatic generation of meaning is not\n"
"perfect, and should be checked before adding cards."
msgstr ""
"\n"
"Le champ lecture est par défaut créé automatiquement,\n"
"et affiche la lecture de l'expression. Pour les mots normalement\n"
"écrits en hiragana ou en katakana et qui n'ont pas besoin\n"
"de lecture, vous pouvez mettre le mot dans le champ expression\n"
"et laisser vide le champ lecture. Une lecture ne sera pas \n"
"automatiquement créée pour les mots écrits seulement en\n"
"hiragana ou katakana.\n"
"\n"
"Notez bien que la création automatique de la signification n'est pas\n"
"parfaite, vous devriez donc la vérifier avant d'ajouter des cartes."
#: stats.py:500
#, python-format
msgid "%(count)s %(gradename)s kanji."
msgstr "%(count)s %(gradename)s kanji."
#: stats.py:498
#, python-format
msgid "%(gradename)s: %(count)s of %(total)s (%(percent)0.1f%%)."
msgstr "%(gradename)s: %(count)s sur %(total)s (%(percent)0.1f%%)."
#: stats.py:277 stats.py:279
#, python-format
msgid "%0.1f seconds"
msgstr "%0.1f secondes"
#: stats.py:262 stats.py:303
#, python-format
msgid "%s ago"
msgstr "il y a %s"
#: utils.py:19
#, python-format
msgid "%s day"
msgid_plural "%s days"
msgstr[0] "%s jour"
msgstr[1] "%s jours"
#: utils.py:20
#, python-format
msgid "%s hour"
msgid_plural "%s hours"
msgstr[0] "%s heure"
msgstr[1] "%s heures"
#: utils.py:21
#, python-format
msgid "%s minute"
msgid_plural "%s minutes"
msgstr[0] "%s minute"
msgstr[1] "%s minutes"
#: utils.py:18
#, python-format
msgid "%s month"
msgid_plural "%s months"
msgstr[0] "%s mois"
msgstr[1] "%s mois"
#: utils.py:22
#, python-format
msgid "%s second"
msgid_plural "%s seconds"
msgstr[0] "%s seconde"
msgstr[1] "%s secondes"
#: utils.py:17
#, python-format
msgid "%s year"
msgid_plural "%s years"
msgstr[0] "%s année"
msgstr[1] "%s années"
#: stats.py:370 stats.py:373 stats.py:376 stats.py:379
#, fuzzy, python-format
msgid "<b>%(a)d</b> (<b>%(b)0.1f</b>/day)"
msgstr "Cartes mûres : <b>%(old)d</b> (<b>%(oldP)0.2f%%</b>)<br>"
#: stats.py:367
#, fuzzy, python-format
msgid "<b>%(a)d</b>/day, <b>%(b)d</b>/mon"
msgstr "Ajouts : <b>%(a)d</b> par jour, <b>%(b)d</b> par mois<br>"
#: stats.py:356
#, fuzzy, python-format
msgid "<b>%0.0f</b> days"
msgstr "Intervalle : <b>%0.0f</b> jours<br/>"
#: stats.py:357 stats.py:359 stats.py:361 stats.py:363 stats.py:365
#, fuzzy, python-format
msgid "<b>%0.1f</b> cards/day"
msgstr "Charge de travail moyenne: <b>%0.1f</b> cartes par jour<br/>"
#: stats.py:354
msgid "<b>Averages</b><br>"
msgstr "<b>Moyennes</b><br>"
#: stats.py:333
msgid "<b>Card counts</b><br>"
msgstr "<b>Décomptes de cartes</b><br>"
#: stats.py:341
msgid "<b>Correct answers</b><br>"
msgstr "<b>Réponses exactes</b><br>"
#: stats.py:522
#, python-format
msgid "<h1>Kanji statistics</h1>The %d seen cards in this deck contain:"
msgstr "<h1>Statistiques relatives aux Kanji</h1>Les %d cartes vues de ce paquet contiennent :"
#: stats.py:526
#, python-format
msgid "<li>%d total unique kanji.</li>"
msgstr "<li>%d kanji différents.</li>"
#: sync.py:171
#, python-format
msgid ""
"<table width=500>\n"
"<tr><td><b>Added/Changed</b></td><td><b>Here</b></td><td><b>Server</b></td></tr>\n"
"<tr><td>Cards</td><td>%(lc)d</td><td>%(rc)d</td></tr>\n"
"<tr><td>Facts</td><td>%(lf)d</td><td>%(rf)d</td></tr>\n"
"<tr><td>Models</td><td>%(lm)d</td><td>%(rm)d</td></tr>\n"
"%(media)s\n"
"</table>"
msgstr ""
#: stdmodels.py:32
msgid ""
"A basic flashcard with a front and a back.\n"
"Questions are asked from front to back by default.\n"
"\n"
"Please consider customizing this model, rather than\n"
"using it verbatim: field names like \"expression\" are\n"
"clearer than \"front\" and \"back\", and will ensure\n"
"that your entries are consistent."
msgstr ""
"Une fiche de révision simple avec un recto et un verso.\n"
"Les questions portent par défaut du recto au verso.\n"
"\n"
"Vous devriez personnaliser ce modèle plutôt que de l'utiliser\n"
"tel quel : des noms de champ comme \"expression\"\n"
"sont plus clairs que \"recto\" et \"verso\", et garantiront\n"
"que vos saisies sont cohérentes. "
#: stdmodels.py:164
msgid "A description in your native language, or Cantonese"
msgstr "Une description dans votre langue maternelle, ou en cantonais"
#: stdmodels.py:75
msgid "A description in your native language, or Japanese"
msgstr "Une description dans votre langue maternelle, ou en japonais"
#: stdmodels.py:192
msgid "A description in your native language, or Mandarin"
msgstr "Une description dans votre langue maternelle, ou en mandarin"
#: stdmodels.py:38
msgid "A question."
msgstr "Une question."
#: stdmodels.py:159 stdmodels.py:186
msgid "A word or expression written in Hanzi."
msgstr "Un mot ou expression écrit en Hanzi."
#: stdmodels.py:65
msgid "A word or expression written in Kanji."
msgstr "Un mot ou expression écrit en Kanji."
#: stdmodels.py:84 stdmodels.py:170 stdmodels.py:199
msgid "Actively test your recall by producing the target expression"
msgstr "Testez activement votre souvenir en produisant l'expression cible"
#: stats.py:256
msgid "Added"
msgstr "Ajoutée"
#: stats.py:373
msgid "Added last month"
msgstr ""
#: stats.py:370
msgid "Added last week"
msgstr ""
#: exporting.py:216
msgid "Anki deck (*.anki)"
msgstr "Paquet Anki (*.anki)"
#: exporting.py:55
msgid "Anki decks (*.anki)"
msgstr "Paquets Anki (*.anki)"
#: deck.py:418
#, python-format
msgid ""
"At the same time tomorrow:<br><br>\n"
"- There will be <b>%(wait)d</b> cards waiting for review<br>\n"
"- There will be <b>%(new)d</b>\n"
"<a href=\"http://ichi2.net/anki/wiki/Key_Terms_and_Concepts#head-061e5433d4571d7ec7ecba0c329c09bd27c84d63\">\n"
"new cards</a> waiting"
msgstr ""
"Au même moment demain :<br><br>\n"
"- attendront <b>%(wait)d</b> cartes pour révision<br>\n"
"- attendront <b>%(new)d</b>\n"
"<a href=\"http://ichi2.net/anki/wiki/Key_Terms_and_Concepts#head-061e5433d4571d7ec7ecba0c329c09bd27c84d63\">\n"
"nouvelles cartes</a>"
#: stdmodels.py:78 stdmodels.py:166 stdmodels.py:194
msgid "Automatically generated by default."
msgstr "Créé automatiquement par défaut"
#: stats.py:357
#, fuzzy
msgid "Average reps"
msgstr "Temps moyen"
#: stats.py:277
msgid "Average time"
msgstr "Temps moyen"
#: stats.py:367
#, fuzzy
msgid "Avg. added"
msgstr "Ajoutée"
#: stdmodels.py:42
msgid "Back to front"
msgstr "D'arrière en avant"
#: stdmodels.py:31
msgid "Basic"
msgstr "Simple"
#: deck.py:1452 deck.py:1525
msgid "Can't read/write deck"
msgstr "Impossible de lire/écrire le paquet"
#: stdmodels.py:156
msgid "Cantonese"
msgstr "Cantonais"
#: stats.py:290
msgid "Card model tags"
msgstr "Marqueurs de modèles de cartes"
#: stats.py:289
msgid "Card tags"
msgstr "Marqueurs de cartes"
#: graphs.py:77 graphs.py:99 graphs.py:113 graphs.py:135
msgid "Cards"
msgstr "Cartes"
#: exporting.py:217
msgid "Cards in tab-separated text file (*.txt)"
msgstr "Cartes dans un fichier texte à séparateur tabulation (*.txt)"
#: models.py:25
msgid "Center"
msgstr "Centre"
#: stats.py:259
msgid "Changed"
msgstr "Changée"
#: stats.py:274
msgid "Correct count"
msgstr "Nombre de fois correctes "
#: stats.py:270
msgid "Current factor"
msgstr "Facteur actuel"
#: stats.py:266
msgid "Current interval"
msgstr "Intervalle actuel"
#: deck.py:1311
msgid "Database file damaged. Restore from backup."
msgstr "Base de données endommagée. Restauration depuis la sauvegarde."
#: graphs.py:136
msgid "Day"
msgstr "Jour"
#: graphs.py:78 graphs.py:100 graphs.py:114
msgid "Days"
msgstr "Jours"
#: stats.py:318
msgid "Deck Statistics"
msgstr "Statistiques de paquet"
#: stats.py:319
#, python-format
msgid "Deck created: <b>%s</b> ago<br>"
msgstr "Paquet créé : il y a <b>%s</b><br>"
#: deck.py:1319
msgid "Deck was missing a model"
msgstr "Il manque un modèle au paquet"
#: deck.py:1340
#, python-format
msgid "Deleted %d cards with missing fact"
msgstr "%d cartes sans faits supprimés"
#: deck.py:1348
#, python-format
msgid "Deleted %d cards with no card model"
msgstr "%d cartes sans modèles supprimées"
#: deck.py:1361
#, python-format
msgid "Deleted %d dangling fields"
msgstr "%d champs en suspens"
#: deck.py:1333
#, python-format
msgid "Deleted %d facts with missing fields"
msgstr "%d faits sans champs supprimés"
#: deck.py:1353
#, python-format
msgid "Deleted %d facts with no cards"
msgstr "%d faits sans cartes supprimés"
#: stdmodels.py:100
msgid "English"
msgstr "Anglais"
#: latex.py:104
msgid "Error executing 'latex' or 'dvipng' - are they installed?"
msgstr "Erreur en tentant d'exécuter 'latex' ou 'dvipng' - Sont-ils installés ?"
#: stats.py:294
msgid "Fact tags"
msgstr "Marqueurs de fait"
#: exporting.py:218
msgid "Facts in tab-separated text file (*.txt)"
msgstr "Faits dans un fichier texte à séparateur tabulation (*.txt)"
#: deck.py:1497
msgid "File is in use by another process"
msgstr "Un autre processus utilise ce fichier"
#: stats.py:379
#, fuzzy
msgid "First last month"
msgstr "Première fois"
#: stats.py:376
#, fuzzy
msgid "First last week"
msgstr "Première fois"
#: stats.py:258
msgid "First review"
msgstr "Première fois"
#: stats.py:282
msgid "First time"
msgstr "Première fois"
#: stats.py:346
#, python-format
msgid "First-seen cards: <b>%(gNewYes%)0.1f%%</b> (<b>%(gNewYes)d</b> of <b>%(gNewTotal)d</b>)<br><br>"
msgstr "Vues pour la première fois : <b>%(gNewYes%)0.1f%%</b> (<b>%(gNewYes)d</b> sur <b>%(gNewTotal)d</b>)<br><br>"
#: stdmodels.py:110
msgid "From the English expression to the meaning."
msgstr "Depuis l'expression en français vers la signification."
#: stdmodels.py:142
msgid "From the keyword to the Kanji."
msgstr "Du mot-clé au Kanji"
#: stdmodels.py:107
msgid "From the meaning to the English expression."
msgstr "Depuis la signification vers l'expression en français."
#: stdmodels.py:40
msgid "Front to back"
msgstr "D'avant en arrière"
#: stdmodels.py:120
msgid "Heisig"
msgstr "Heisig"
#: stats.py:356
#, fuzzy
msgid "Interval"
msgstr "Intervalle précédent"
#: stdmodels.py:51
msgid "Japanese"
msgstr "Japonais"
#: stats.py:537
msgid "Jouyou levels:"
msgstr "Niveaux Jouyou"
#: stats.py:271
msgid "Last factor"
msgstr "Facteur précédent"
#: stats.py:269
msgid "Last interval"
msgstr "Intervalle précédent"
#: models.py:26
msgid "Left"
msgstr "Gauche"
#: stdmodels.py:183
msgid "Mandarin"
msgstr "Mandarin"
#: stats.py:286
msgid "Mature"
msgstr "Mûre"
#: graphs.py:210
msgid "Mature cards"
msgstr "Cartes mûres"
#: stats.py:342
#, python-format
msgid "Mature cards: <b>%(gMatureYes%)0.1f%%</b> (<b>%(gMatureYes)d</b> of <b>%(gMatureTotal)d</b>)<br>"
msgstr "Cartes mûres : <b>%(gMatureYes%)0.1f%%</b> (<b>%(gMatureYes)d</b> sur <b>%(gMatureTotal)d</b>)<br>"
#: stats.py:334
#, python-format
msgid "Mature cards: <b>%(old)d</b> (<b>%(oldP)0.2f%%</b>)<br>"
msgstr "Cartes mûres : <b>%(old)d</b> (<b>%(oldP)0.2f%%</b>)<br>"
#: media.py:161 media.py:163 media.py:164
msgid "Media Missing"
msgstr ""
#: deck.py:915
#, python-format
msgid "Model '%s' has wrong card model count"
msgstr "Le modèle '%s' avait le mauvais nombre de cartes"
#: deck.py:919
#, python-format
msgid "Model '%s' has wrong field model count"
msgstr "Le modèle '%s' avait le mauvais nombre de champs"
#: stats.py:292
msgid "Model tags"
msgstr "Marqueurs de modèle"
#: graphs.py:208
msgid "New cards"
msgstr "Nouvelles cartes"
#: stats.py:265
msgid "Next due"
msgstr "Prochaine échéance"
#: stats.py:316
msgid "Please add some cards first.<p/>"
msgstr "Veuillez d'abord ajouter des cartes.<p/>"
#: stats.py:276
msgid "Repeatedly correct"
msgstr "Répétitions correctes"
#: stats.py:365
#, fuzzy
msgid "Reps last month"
msgstr "%s mois"
#: stats.py:363
msgid "Reps last week"
msgstr ""
#: stats.py:361
#, fuzzy
msgid "Reps next month"
msgstr "%s mois"
#: stats.py:359
msgid "Reps next week"
msgstr ""
#: stats.py:272
msgid "Review count"
msgstr "Nombre de révisions"
#: models.py:27
msgid "Right"
msgstr "Droite"
#: deck.py:1824
#, fuzzy
msgid "Show new cards after all other cards"
msgstr "Montrer les nouvelles cartes dans l'ordre de leur ajout"
#: deck.py:1818
msgid "Show new cards in order they were added"
msgstr "Montrer les nouvelles cartes dans l'ordre de leur ajout"
#: deck.py:1817
msgid "Show new cards in random order"
msgstr "Montrer les nouvelles cartes au hasard"
#: deck.py:1823
msgid "Spread new cards out through reviews"
msgstr ""
#: stats.py:287
msgid "State"
msgstr "État"
#: stdmodels.py:88 stdmodels.py:174 stdmodels.py:203
msgid "Test your ability to recognize the target expression"
msgstr "Testez votre capacité à reconnaître l'expression cible"
#: exporting.py:145 exporting.py:177
msgid "Text files (*.txt)"
msgstr "Fichiers texte (*.txt)"
#: stdmodels.py:39
msgid "The answer."
msgstr "La réponse."
#: deck.py:1325
msgid "The current model didn't exist"
msgstr "Le modèle courant n'existait pas"
#: deck.py:431
#, fuzzy
msgid "The deck is empty. Please add some cards."
msgstr "Veuillez d'abord ajouter des cartes.<p/>"
#: deck.py:428
#, python-format
msgid "The next card will be shown in <b>%s</b>"
msgstr "La prochaine carte apparaîtra dans <b>%s</b>"
#: stats.py:331
#, python-format
msgid "Total number of cards: <b>%d</b><br><br>"
msgstr "Nombre total de cartes : <b>%d</b><br><br>"
#: stats.py:279
msgid "Total time"
msgstr "Temps total"
#: deck.py:702
msgid "Unknown"
msgstr "Inconnu"
#: stats.py:338
#, python-format
msgid "Unseen cards: <b>%(new)d</b> (<b>%(newP)0.2f%%</b>)<br><br>"
msgstr "Cartes non vues: <b>%(new)d</b> (<b>%(newP)0.2f%%</b>)<br><br>"
#: stats.py:284
msgid "Young"
msgstr "Récente"
#: graphs.py:209
msgid "Young cards"
msgstr "Cartes récentes"
#: stats.py:344
#, python-format
msgid "Young cards: <b>%(gYoungYes%)0.1f%%</b> (<b>%(gYoungYes)d</b> of <b>%(gYoungTotal)d</b>)<br>"
msgstr "Cartes jeunes : <b>%(gYoungYes%)0.1f%%</b> (<b>%(gYoungYes)d</b> sur <b>%(gYoungTotal)d</b>)<br>"
#: stats.py:336
#, python-format
msgid "Young cards: <b>%(young)d</b> (<b>%(youngP)0.2f%%</b>)<br>"
msgstr "Cartes jeunes : <b>%(young)d</b> (<b>%(youngP)0.2f%%</b>)<br>"
#: models.py:150 models.py:151 models.py:182
msgid "[empty]"
msgstr "[vide]"
#: models.py:148
msgid "[invalid format; see model properties]"
msgstr "[format invalide ; voir les propriétés du modèle]"
#: deck.py:463
msgid "a short time"
msgstr "un temps court"
#: stats.py:264
#, python-format
msgid "in %s"
msgstr "dans %s"
#: deck.py:448
msgid "unknown"
msgstr "inconnu"
#~ msgid ""
#~ "\n"
#~ "Two cards: one tests meaning -> imperfective + perfective,\n"
#~ "and one tests imperfective -> perfective."
#~ msgstr ""
#~ "\n"
#~ "Deux cartes : l'une teste signification -> imperfectif + perfectif,\n"
#~ " et l'autre teste imperfectif -> perfectif."
#~ msgid ""
#~ "\n"
#~ "Two cards: one tests production, one recognition."
#~ msgstr ""
#~ "\n"
#~ "Deux cartes: l'une teste la production, l'autre la reconnaissance."
#~ msgid "%(a)d.%(b)df day"
#~ msgstr "%(a)d.%(b)df jour"
#~ msgid "%(a)d.%(b)df hour"
#~ msgstr "%(a)d.%(b)df heure"
#~ msgid "%(a)d.%(b)df minute"
#~ msgstr "%(a)d.%(b)df minute"
#~ msgid "%(a)d.%(b)df second"
#~ msgstr "%(a)d.%(b)df seconde"
#~ msgid "%(a)d.%(b)df year"
#~ msgstr "%(a)d.%(b)df année"
#~ msgid "%s days"
#~ msgstr "%s jours"
#~ msgid "%x at %H:%M"
#~ msgstr "%x à %H:%M"
#~ msgid "A random position in the deck"
#~ msgstr "Une position aléatoire dans le paquet"
#~ msgid "At the end of the deck"
#~ msgstr "À la fin du paquet"
#~ msgid "At the start of the deck"
#~ msgstr "Au début du paquet"
#~ msgid "Can't read/write directory"
#~ msgstr "Impossible de lire/écrire le répertoire"
#~ msgid "Colon/semicolon/tab-separated text file (*.*)"
#~ msgstr "Fichier texte à séparateur virgule/point-virgule/tabulation (*.*)"
#~ msgid "Couldn't find pattern. The file should be a series of lines separated by colons, semicolons or tabs."
#~ msgstr "Impossible de trouver le motif. Le fichier doit être une série de lignes séparées par des virgules, des points-virgules ou des tabulations."
#~ msgid "Deck%"
#~ msgstr "Paquet%"
#~ msgid "Fc .pending file (*.pending *.imported)"
#~ msgstr "Fichier Fc .pending (*.pending *.imported)"
#~ msgid "From imperfective to perfective"
#~ msgstr "De l'imparfait au parfait"
#~ msgid "From the Russian expression to the meaning."
#~ msgstr "Depuis l'expression en russe vers la signification."
#~ msgid "From the meaning to an imperfective+perfective pair."
#~ msgstr "Depuis la signification vers un couple perfectif+ imperfectif"
#~ msgid "From the meaning to the Russian expression."
#~ msgstr "Depuis la signification vers l'expression en russe."
#~ msgid "Import: duplicate"
#~ msgstr "Importation : double"
#~ msgid "Import: missing field"
#~ msgstr "Importation : champ manquant"
#~ msgid "Import: same %s"
#~ msgstr "Importation : même %s"
#~ msgid "Line %(line)d doesn't match pattern '%(pat)s'"
#~ msgstr "La ligne %(line)d ne correspond pas au modèle '%(pat)s'"
#~ msgid "Line %(line)d had %(num1)d fields, expected %(num2)d"
#~ msgstr "La ligne %(line)d avait %(num1)d champs au lieu de %(num2)d prévus"
#~ msgid "Mnemosyne file (*.mem)"
#~ msgstr "Fichier Mnemosyne (*.mem)"
#~ msgid "Model"
#~ msgstr "Modèle"
#~ msgid "Old Anki 0.2.x file (*.fc)"
#~ msgstr "Ancien fichier Anki 0.2.x (*.fc)"
#~ msgid "Old cards"
#~ msgstr "Cartes anciennes"
#~ msgid "Other"
#~ msgstr "Autre"
#~ msgid "Period"
#~ msgstr "Période"
#~ msgid "Russian"
#~ msgstr "russe"
#~ msgid "Russian verbs (imperfective + perfective)"
#~ msgstr "Verbes russes (imparfait + parfait)"
#~ msgid "The file had no non-empty lines."
#~ msgstr "Le fichier avait des lignes non vides."
#~ msgid "The file was not in UTF8 format."
#~ msgstr "Le fichier n'était pas au format UTF-8"
#~ msgid "Untitled"
#~ msgstr "Sans titre"
#~ msgid "in %(a)s, on %(b)s"
#~ msgstr "dans %(a)s, sur %(b)s"

View file

@ -0,0 +1,638 @@
# Japanese translations for libanki
# Copyright (C) 2007 Damien Elmes
# This file is distributed under the same license as the libanki package.
# Damien Elmes <anki@ichi2.net>, 2007.
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2008-09-23 03:21+0900\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Damien Elmes <anki@ichi2.net>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: deck.py:483
#, python-format
msgid ""
"\n"
"<h1>Congratulations!</h1>You have finished the deck for now.<br><br>\n"
"%(next)s\n"
"<br><br>\n"
"- There are <b>%(waiting)d</b>\n"
"<a href=\"http://ichi2.net/anki/wiki/Key_Terms_and_Concepts#head-59a81e35b6afb23930005e943068945214d194b3\">\n"
"spaced</a> cards.<br>\n"
"- There are <b>%(suspended)d</b>\n"
"<a href=\"http://ichi2.net/anki/wiki/Key_Terms_and_Concepts#head-37d2db274e6caa23aef55e29655a6b806901774b\">\n"
"suspended</a> cards."
msgstr ""
#: stdmodels.py:121
msgid ""
"\n"
"A format suitable for Heisig's \"Remembering the Kanji\".\n"
"You are tested from the keyword to the kanji.\n"
"\n"
"Layout of the test is based on the great work at\n"
"http://kanji.koohii.com/\n"
"\n"
"The link in the question will list user-contributed\n"
"stories. A free login is required."
msgstr ""
#: stdmodels.py:101
msgid ""
"\n"
"Enter the English expression you want to learn in the 'Expression' field.\n"
"Enter a description in Japanese or English in the 'Meaning' field."
msgstr ""
#: stdmodels.py:52
msgid ""
"\n"
"The reading field is automatically generated by default,\n"
"and shows the reading for the expression. For words that\n"
"are normally written in hiragana or katakana and don't\n"
"need a reading, you can put the word in the expression\n"
"field, and leave the reading field blank. A reading will\n"
"will not automatically be generated for words written\n"
"in only hiragana or katakana.\n"
"\n"
"Note that the automatic generation of meaning is not\n"
"perfect, and should be checked before adding cards."
msgstr ""
#: stats.py:500
#, python-format
msgid "%(count)s %(gradename)s kanji."
msgstr ""
#: stats.py:498
#, python-format
msgid "%(gradename)s: %(count)s of %(total)s (%(percent)0.1f%%)."
msgstr ""
#: stats.py:277 stats.py:279
#, python-format
msgid "%0.1f seconds"
msgstr ""
#: stats.py:262 stats.py:303
#, python-format
msgid "%s ago"
msgstr "%s前"
#: utils.py:19
#, python-format
msgid "%s day"
msgid_plural "%s days"
msgstr[0] "%s日"
msgstr[1] "%s日"
#: utils.py:20
#, python-format
msgid "%s hour"
msgid_plural "%s hours"
msgstr[0] "%s時間"
msgstr[1] "%s時間"
#: utils.py:21
#, python-format
msgid "%s minute"
msgid_plural "%s minutes"
msgstr[0] "%s分"
msgstr[1] "%s分"
#: utils.py:18
#, python-format
msgid "%s month"
msgid_plural "%s months"
msgstr[0] "%sヶ月"
msgstr[1] "%sヶ月"
#: utils.py:22
#, python-format
msgid "%s second"
msgid_plural "%s seconds"
msgstr[0] "%s秒"
msgstr[1] "%s秒"
#: utils.py:17
#, python-format
msgid "%s year"
msgid_plural "%s years"
msgstr[0] "%s年"
msgstr[1] "%s年"
#: stats.py:370 stats.py:373 stats.py:376 stats.py:379
#, python-format
msgid "<b>%(a)d</b> (<b>%(b)0.1f</b>/day)"
msgstr ""
#: stats.py:367
#, python-format
msgid "<b>%(a)d</b>/day, <b>%(b)d</b>/mon"
msgstr ""
#: stats.py:356
#, python-format
msgid "<b>%0.0f</b> days"
msgstr ""
#: stats.py:357 stats.py:359 stats.py:361 stats.py:363 stats.py:365
#, python-format
msgid "<b>%0.1f</b> cards/day"
msgstr ""
#: stats.py:354
msgid "<b>Averages</b><br>"
msgstr ""
#: stats.py:333
msgid "<b>Card counts</b><br>"
msgstr ""
#: stats.py:341
msgid "<b>Correct answers</b><br>"
msgstr ""
#: stats.py:522
#, python-format
msgid "<h1>Kanji statistics</h1>The %d seen cards in this deck contain:"
msgstr ""
#: stats.py:526
#, python-format
msgid "<li>%d total unique kanji.</li>"
msgstr ""
#: sync.py:171
#, python-format
msgid ""
"<table width=500>\n"
"<tr><td><b>Added/Changed</b></td><td><b>Here</b></td><td><b>Server</b></td></tr>\n"
"<tr><td>Cards</td><td>%(lc)d</td><td>%(rc)d</td></tr>\n"
"<tr><td>Facts</td><td>%(lf)d</td><td>%(rf)d</td></tr>\n"
"<tr><td>Models</td><td>%(lm)d</td><td>%(rm)d</td></tr>\n"
"%(media)s\n"
"</table>"
msgstr ""
#: stdmodels.py:32
msgid ""
"A basic flashcard with a front and a back.\n"
"Questions are asked from front to back by default.\n"
"\n"
"Please consider customizing this model, rather than\n"
"using it verbatim: field names like \"expression\" are\n"
"clearer than \"front\" and \"back\", and will ensure\n"
"that your entries are consistent."
msgstr ""
#: stdmodels.py:164
msgid "A description in your native language, or Cantonese"
msgstr ""
#: stdmodels.py:75
msgid "A description in your native language, or Japanese"
msgstr ""
#: stdmodels.py:192
msgid "A description in your native language, or Mandarin"
msgstr ""
#: stdmodels.py:38
msgid "A question."
msgstr ""
#: stdmodels.py:159 stdmodels.py:186
msgid "A word or expression written in Hanzi."
msgstr ""
#: stdmodels.py:65
msgid "A word or expression written in Kanji."
msgstr ""
#: stdmodels.py:84 stdmodels.py:170 stdmodels.py:199
msgid "Actively test your recall by producing the target expression"
msgstr ""
#: stats.py:256
msgid "Added"
msgstr ""
#: stats.py:373
msgid "Added last month"
msgstr ""
#: stats.py:370
msgid "Added last week"
msgstr ""
#: exporting.py:216
msgid "Anki deck (*.anki)"
msgstr ""
#: exporting.py:55
msgid "Anki decks (*.anki)"
msgstr ""
#: deck.py:418
#, python-format
msgid ""
"At the same time tomorrow:<br><br>\n"
"- There will be <b>%(wait)d</b> cards waiting for review<br>\n"
"- There will be <b>%(new)d</b>\n"
"<a href=\"http://ichi2.net/anki/wiki/Key_Terms_and_Concepts#head-061e5433d4571d7ec7ecba0c329c09bd27c84d63\">\n"
"new cards</a> waiting"
msgstr ""
#: stdmodels.py:78 stdmodels.py:166 stdmodels.py:194
msgid "Automatically generated by default."
msgstr ""
#: stats.py:357
msgid "Average reps"
msgstr ""
#: stats.py:277
msgid "Average time"
msgstr ""
#: stats.py:367
msgid "Avg. added"
msgstr ""
#: stdmodels.py:42
msgid "Back to front"
msgstr "裏側から表側"
#: stdmodels.py:31
msgid "Basic"
msgstr ""
#: deck.py:1452 deck.py:1525
msgid "Can't read/write deck"
msgstr ""
#: stdmodels.py:156
msgid "Cantonese"
msgstr ""
#: stats.py:290
msgid "Card model tags"
msgstr ""
#: stats.py:289
msgid "Card tags"
msgstr ""
#: graphs.py:77 graphs.py:99 graphs.py:113 graphs.py:135
msgid "Cards"
msgstr ""
#: exporting.py:217
msgid "Cards in tab-separated text file (*.txt)"
msgstr ""
#: models.py:25
msgid "Center"
msgstr ""
#: stats.py:259
msgid "Changed"
msgstr ""
#: stats.py:274
msgid "Correct count"
msgstr ""
#: stats.py:270
msgid "Current factor"
msgstr ""
#: stats.py:266
msgid "Current interval"
msgstr ""
#: deck.py:1311
msgid "Database file damaged. Restore from backup."
msgstr ""
#: graphs.py:136
msgid "Day"
msgstr ""
#: graphs.py:78 graphs.py:100 graphs.py:114
msgid "Days"
msgstr ""
#: stats.py:318
msgid "Deck Statistics"
msgstr ""
#: stats.py:319
#, python-format
msgid "Deck created: <b>%s</b> ago<br>"
msgstr ""
#: deck.py:1319
msgid "Deck was missing a model"
msgstr ""
#: deck.py:1340
#, python-format
msgid "Deleted %d cards with missing fact"
msgstr ""
#: deck.py:1348
#, python-format
msgid "Deleted %d cards with no card model"
msgstr ""
#: deck.py:1361
#, python-format
msgid "Deleted %d dangling fields"
msgstr ""
#: deck.py:1333
#, python-format
msgid "Deleted %d facts with missing fields"
msgstr ""
#: deck.py:1353
#, python-format
msgid "Deleted %d facts with no cards"
msgstr ""
#: stdmodels.py:100
msgid "English"
msgstr ""
#: latex.py:104
msgid "Error executing 'latex' or 'dvipng' - are they installed?"
msgstr ""
#: stats.py:294
msgid "Fact tags"
msgstr ""
#: exporting.py:218
msgid "Facts in tab-separated text file (*.txt)"
msgstr ""
#: deck.py:1497
msgid "File is in use by another process"
msgstr ""
#: stats.py:379
msgid "First last month"
msgstr ""
#: stats.py:376
msgid "First last week"
msgstr ""
#: stats.py:258
msgid "First review"
msgstr ""
#: stats.py:282
msgid "First time"
msgstr ""
#: stats.py:346
#, python-format
msgid "First-seen cards: <b>%(gNewYes%)0.1f%%</b> (<b>%(gNewYes)d</b> of <b>%(gNewTotal)d</b>)<br><br>"
msgstr ""
#: stdmodels.py:110
msgid "From the English expression to the meaning."
msgstr ""
#: stdmodels.py:142
msgid "From the keyword to the Kanji."
msgstr ""
#: stdmodels.py:107
msgid "From the meaning to the English expression."
msgstr ""
#: stdmodels.py:40
msgid "Front to back"
msgstr "表側から裏側"
#: stdmodels.py:120
msgid "Heisig"
msgstr ""
#: stats.py:356
msgid "Interval"
msgstr ""
#: stdmodels.py:51
msgid "Japanese"
msgstr ""
#: stats.py:537
msgid "Jouyou levels:"
msgstr ""
#: stats.py:271
msgid "Last factor"
msgstr ""
#: stats.py:269
msgid "Last interval"
msgstr ""
#: models.py:26
msgid "Left"
msgstr ""
#: stdmodels.py:183
msgid "Mandarin"
msgstr ""
#: stats.py:286
msgid "Mature"
msgstr ""
#: graphs.py:210
msgid "Mature cards"
msgstr ""
#: stats.py:342
#, python-format
msgid "Mature cards: <b>%(gMatureYes%)0.1f%%</b> (<b>%(gMatureYes)d</b> of <b>%(gMatureTotal)d</b>)<br>"
msgstr ""
#: stats.py:334
#, python-format
msgid "Mature cards: <b>%(old)d</b> (<b>%(oldP)0.2f%%</b>)<br>"
msgstr ""
#: media.py:161 media.py:163 media.py:164
msgid "Media Missing"
msgstr ""
#: deck.py:915
#, python-format
msgid "Model '%s' has wrong card model count"
msgstr ""
#: deck.py:919
#, python-format
msgid "Model '%s' has wrong field model count"
msgstr ""
#: stats.py:292
msgid "Model tags"
msgstr ""
#: graphs.py:208
msgid "New cards"
msgstr ""
#: stats.py:265
msgid "Next due"
msgstr ""
#: stats.py:316
msgid "Please add some cards first.<p/>"
msgstr ""
#: stats.py:276
msgid "Repeatedly correct"
msgstr ""
#: stats.py:365
#, fuzzy
msgid "Reps last month"
msgstr "%sヶ月"
#: stats.py:363
msgid "Reps last week"
msgstr ""
#: stats.py:361
#, fuzzy
msgid "Reps next month"
msgstr "%sヶ月"
#: stats.py:359
msgid "Reps next week"
msgstr ""
#: stats.py:272
msgid "Review count"
msgstr ""
#: models.py:27
msgid "Right"
msgstr ""
#: deck.py:1824
msgid "Show new cards after all other cards"
msgstr ""
#: deck.py:1818
msgid "Show new cards in order they were added"
msgstr ""
#: deck.py:1817
msgid "Show new cards in random order"
msgstr ""
#: deck.py:1823
msgid "Spread new cards out through reviews"
msgstr ""
#: stats.py:287
msgid "State"
msgstr ""
#: stdmodels.py:88 stdmodels.py:174 stdmodels.py:203
msgid "Test your ability to recognize the target expression"
msgstr ""
#: exporting.py:145 exporting.py:177
msgid "Text files (*.txt)"
msgstr ""
#: stdmodels.py:39
msgid "The answer."
msgstr ""
#: deck.py:1325
msgid "The current model didn't exist"
msgstr ""
#: deck.py:431
msgid "The deck is empty. Please add some cards."
msgstr ""
#: deck.py:428
#, python-format
msgid "The next card will be shown in <b>%s</b>"
msgstr ""
#: stats.py:331
#, python-format
msgid "Total number of cards: <b>%d</b><br><br>"
msgstr ""
#: stats.py:279
msgid "Total time"
msgstr ""
#: deck.py:702
msgid "Unknown"
msgstr ""
#: stats.py:338
#, python-format
msgid "Unseen cards: <b>%(new)d</b> (<b>%(newP)0.2f%%</b>)<br><br>"
msgstr ""
#: stats.py:284
msgid "Young"
msgstr ""
#: graphs.py:209
msgid "Young cards"
msgstr ""
#: stats.py:344
#, python-format
msgid "Young cards: <b>%(gYoungYes%)0.1f%%</b> (<b>%(gYoungYes)d</b> of <b>%(gYoungTotal)d</b>)<br>"
msgstr ""
#: stats.py:336
#, python-format
msgid "Young cards: <b>%(young)d</b> (<b>%(youngP)0.2f%%</b>)<br>"
msgstr ""
#: models.py:150 models.py:151 models.py:182
msgid "[empty]"
msgstr ""
#: models.py:148
msgid "[invalid format; see model properties]"
msgstr ""
#: deck.py:463
msgid "a short time"
msgstr "何分"
#: stats.py:264
#, python-format
msgid "in %s"
msgstr ""
#: deck.py:448
msgid "unknown"
msgstr ""

View file

@ -0,0 +1,677 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2008-09-23 03:21+0900\n"
"PO-Revision-Date: 2008-05-24 09:18+0900\n"
"Last-Translator: Jin Eun-Deok <jin.eundeok@gmail.com>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: deck.py:483
#, python-format
msgid ""
"\n"
"<h1>Congratulations!</h1>You have finished the deck for now.<br><br>\n"
"%(next)s\n"
"<br><br>\n"
"- There are <b>%(waiting)d</b>\n"
"<a href=\"http://ichi2.net/anki/wiki/Key_Terms_and_Concepts#head-59a81e35b6afb23930005e943068945214d194b3\">\n"
"spaced</a> cards.<br>\n"
"- There are <b>%(suspended)d</b>\n"
"<a href=\"http://ichi2.net/anki/wiki/Key_Terms_and_Concepts#head-37d2db274e6caa23aef55e29655a6b806901774b\">\n"
"suspended</a> cards."
msgstr ""
#: stdmodels.py:121
msgid ""
"\n"
"A format suitable for Heisig's \"Remembering the Kanji\".\n"
"You are tested from the keyword to the kanji.\n"
"\n"
"Layout of the test is based on the great work at\n"
"http://kanji.koohii.com/\n"
"\n"
"The link in the question will list user-contributed\n"
"stories. A free login is required."
msgstr ""
"\n"
"Heisig의 일본어 한자 교재 \"Remembering the Kanji\"에 적합한 모델.\n"
"뜻을 보고 해당 한자를 맞춥니다.\n"
"\n"
"질문 형식은 http://kanji.koohii.com/을 바탕으로 했습니다.\n"
"\n"
"질문에 표시되는 링크는 회원들이 제공한 해설 페이지로 연결됩니다.\n"
"해당 사이트로 로그인을 해야 합니다."
#: stdmodels.py:101
msgid ""
"\n"
"Enter the English expression you want to learn in the 'Expression' field.\n"
"Enter a description in Japanese or English in the 'Meaning' field."
msgstr ""
"\n"
"공부하고 싶은 영어 표현을 'Expression' 필드에 입력합니다.\n"
"영어 표현의 뜻을 한국어나 영어로 'Meaning' 필드에 입력합니다."
#: stdmodels.py:52
msgid ""
"\n"
"The reading field is automatically generated by default,\n"
"and shows the reading for the expression. For words that\n"
"are normally written in hiragana or katakana and don't\n"
"need a reading, you can put the word in the expression\n"
"field, and leave the reading field blank. A reading will\n"
"will not automatically be generated for words written\n"
"in only hiragana or katakana.\n"
"\n"
"Note that the automatic generation of meaning is not\n"
"perfect, and should be checked before adding cards."
msgstr ""
"\n"
"reading(독음) 필드는 기본적으로 자동 입력되어\n"
"expression(표현) 필드의 일본어를 읽는 법을 보여줍니다.\n"
"히라카나와 카타카나로 이루어진 표현은 따로 독음이 필요없습니다.\n"
"따라서 expression 필드만 입력하고 reading 필드는 비워 둬도 됩니다.\n"
"히라카나와 카타카나로만 구성된 표현에 대해서는\n"
"자동으로 독음을 만들지 않습니다.\n"
"\n"
"자동 생성된 독음은 틀릴 수 있습니다.\n"
"카드를 추가하기 전에 다시 확인해 주세요."
#: stats.py:500
#, python-format
msgid "%(count)s %(gradename)s kanji."
msgstr "%(count)s %(gradename)s kanji."
#: stats.py:498
#, python-format
msgid "%(gradename)s: %(count)s of %(total)s (%(percent)0.1f%%)."
msgstr "%(gradename)s: %(total)s자 중에 %(count)s자 (%(percent)0.1f%%)."
#: stats.py:277 stats.py:279
#, python-format
msgid "%0.1f seconds"
msgstr "%0.1f초"
#: stats.py:262 stats.py:303
#, python-format
msgid "%s ago"
msgstr "%s전"
#: utils.py:19
#, python-format
msgid "%s day"
msgid_plural "%s days"
msgstr[0] "%s일"
msgstr[1] "%s일"
#: utils.py:20
#, python-format
msgid "%s hour"
msgid_plural "%s hours"
msgstr[0] "%s시간"
msgstr[1] "%s시간"
#: utils.py:21
#, python-format
msgid "%s minute"
msgid_plural "%s minutes"
msgstr[0] "%s분"
msgstr[1] "%s분"
#: utils.py:18
#, python-format
msgid "%s month"
msgid_plural "%s months"
msgstr[0] "%s개월"
msgstr[1] "%s개월"
#: utils.py:22
#, python-format
msgid "%s second"
msgid_plural "%s seconds"
msgstr[0] "%s초"
msgstr[1] "%s초"
#: utils.py:17
#, python-format
msgid "%s year"
msgid_plural "%s years"
msgstr[0] "%s년"
msgstr[1] "%s년"
#: stats.py:370 stats.py:373 stats.py:376 stats.py:379
#, fuzzy, python-format
msgid "<b>%(a)d</b> (<b>%(b)0.1f</b>/day)"
msgstr "성숙한 카드: <b>%(old)d</b> (<b>%(oldP)0.2f%%</b>)<br>"
#: stats.py:367
#, fuzzy, python-format
msgid "<b>%(a)d</b>/day, <b>%(b)d</b>/mon"
msgstr "카드 추가: 하루에 <b>%(a)d</b>개, 한달에 <b>%(b)d</b>개<br>"
#: stats.py:356
#, fuzzy, python-format
msgid "<b>%0.0f</b> days"
msgstr "복습 간격: <b>%0.0f</b>일<br/>"
#: stats.py:357 stats.py:359 stats.py:361 stats.py:363 stats.py:365
#, fuzzy, python-format
msgid "<b>%0.1f</b> cards/day"
msgstr "학습량: <b>%0.1f</b> 카드/일<br/>"
#: stats.py:354
msgid "<b>Averages</b><br>"
msgstr "<b>평균</b><br>"
#: stats.py:333
msgid "<b>Card counts</b><br>"
msgstr "<b>카드 개수</b><br>"
#: stats.py:341
msgid "<b>Correct answers</b><br>"
msgstr "<b>정답률</b><br>"
#: stats.py:522
#, python-format
msgid "<h1>Kanji statistics</h1>The %d seen cards in this deck contain:"
msgstr "<h1>일본 한자 통계</h1>현재 묶음에서 지금까지 본 %d개의 카드 중:"
#: stats.py:526
#, python-format
msgid "<li>%d total unique kanji.</li>"
msgstr "<li>전체 한자 %d자</li>"
#: sync.py:171
#, python-format
msgid ""
"<table width=500>\n"
"<tr><td><b>Added/Changed</b></td><td><b>Here</b></td><td><b>Server</b></td></tr>\n"
"<tr><td>Cards</td><td>%(lc)d</td><td>%(rc)d</td></tr>\n"
"<tr><td>Facts</td><td>%(lf)d</td><td>%(rf)d</td></tr>\n"
"<tr><td>Models</td><td>%(lm)d</td><td>%(rm)d</td></tr>\n"
"%(media)s\n"
"</table>"
msgstr ""
#: stdmodels.py:32
msgid ""
"A basic flashcard with a front and a back.\n"
"Questions are asked from front to back by default.\n"
"\n"
"Please consider customizing this model, rather than\n"
"using it verbatim: field names like \"expression\" are\n"
"clearer than \"front\" and \"back\", and will ensure\n"
"that your entries are consistent."
msgstr ""
"앞면과 뒷면으로 이루어진 단순한 플래시 카드\n"
"기본적으로 앞면을 질문합니다.\n"
"\n"
"이 모델을 그대로 쓰기보다는 상황에 맞게\n"
"수정해서 사용하세요. 예를 들어 \"expression(표현)\"이라는 필드 이름이\n"
"\"front(앞면)\"이나 \"back(뒷면)\"보다 필드에 들어갈 내용을\n"
"분명하게 알려 줍니다."
#: stdmodels.py:164
msgid "A description in your native language, or Cantonese"
msgstr "한국어나 광동어 해설"
#: stdmodels.py:75
msgid "A description in your native language, or Japanese"
msgstr "한국어나 일본어 해설"
#: stdmodels.py:192
msgid "A description in your native language, or Mandarin"
msgstr "한국어나 북경어 해설"
#: stdmodels.py:38
msgid "A question."
msgstr "질문."
#: stdmodels.py:159 stdmodels.py:186
msgid "A word or expression written in Hanzi."
msgstr "중국 한자로 쓴 단어나 표현"
#: stdmodels.py:65
msgid "A word or expression written in Kanji."
msgstr "일본 한자로 쓴 단어나 표현"
#: stdmodels.py:84 stdmodels.py:170 stdmodels.py:199
msgid "Actively test your recall by producing the target expression"
msgstr "해당 표현을 만들어 보면서 능동적으로 기억 해보세요"
#: stats.py:256
msgid "Added"
msgstr "추가한 날"
#: stats.py:373
msgid "Added last month"
msgstr ""
#: stats.py:370
msgid "Added last week"
msgstr ""
#: exporting.py:216
msgid "Anki deck (*.anki)"
msgstr "앙키 묶음 (*.anki)"
#: exporting.py:55
msgid "Anki decks (*.anki)"
msgstr "앙키 묶음 (*.anki)"
#: deck.py:418
#, python-format
msgid ""
"At the same time tomorrow:<br><br>\n"
"- There will be <b>%(wait)d</b> cards waiting for review<br>\n"
"- There will be <b>%(new)d</b>\n"
"<a href=\"http://ichi2.net/anki/wiki/Key_Terms_and_Concepts#head-061e5433d4571d7ec7ecba0c329c09bd27c84d63\">\n"
"new cards</a> waiting"
msgstr ""
#: stdmodels.py:78 stdmodels.py:166 stdmodels.py:194
msgid "Automatically generated by default."
msgstr "자동으로 생성됨"
#: stats.py:357
#, fuzzy
msgid "Average reps"
msgstr "평균 시간"
#: stats.py:277
msgid "Average time"
msgstr "평균 시간"
#: stats.py:367
#, fuzzy
msgid "Avg. added"
msgstr "추가한 날"
#: stdmodels.py:42
msgid "Back to front"
msgstr "뒷면에서 앞면으로"
#: stdmodels.py:31
msgid "Basic"
msgstr "기본"
#: deck.py:1452 deck.py:1525
msgid "Can't read/write deck"
msgstr "묶음을 읽고 쓸 수 없음"
#: stdmodels.py:156
msgid "Cantonese"
msgstr "광동어"
#: stats.py:290
msgid "Card model tags"
msgstr "카드 모델 꼬리표"
#: stats.py:289
msgid "Card tags"
msgstr "카드 꼬리표"
#: graphs.py:77 graphs.py:99 graphs.py:113 graphs.py:135
#, fuzzy
msgid "Cards"
msgstr "카드 꼬리표"
#: exporting.py:217
msgid "Cards in tab-separated text file (*.txt)"
msgstr "탭으로 구분한 카드 텍스트 파일 (*.txt)"
#: models.py:25
msgid "Center"
msgstr "가운데"
#: stats.py:259
msgid "Changed"
msgstr "변경"
#: stats.py:274
msgid "Correct count"
msgstr "맞춘 횟수"
#: stats.py:270
msgid "Current factor"
msgstr "현재 인수"
#: stats.py:266
msgid "Current interval"
msgstr "현재 복습 간격"
#: deck.py:1311
msgid "Database file damaged. Restore from backup."
msgstr ""
#: graphs.py:136
msgid "Day"
msgstr ""
#: graphs.py:78 graphs.py:100 graphs.py:114
msgid "Days"
msgstr ""
#: stats.py:318
msgid "Deck Statistics"
msgstr "묶음 통계"
#: stats.py:319
#, python-format
msgid "Deck created: <b>%s</b> ago<br>"
msgstr "묶음 생성: <b>%s</b> 전<br>"
#: deck.py:1319
msgid "Deck was missing a model"
msgstr ""
#: deck.py:1340
#, python-format
msgid "Deleted %d cards with missing fact"
msgstr ""
#: deck.py:1348
#, python-format
msgid "Deleted %d cards with no card model"
msgstr ""
#: deck.py:1361
#, python-format
msgid "Deleted %d dangling fields"
msgstr ""
#: deck.py:1333
#, python-format
msgid "Deleted %d facts with missing fields"
msgstr ""
#: deck.py:1353
#, python-format
msgid "Deleted %d facts with no cards"
msgstr ""
#: stdmodels.py:100
msgid "English"
msgstr "영어"
#: latex.py:104
msgid "Error executing 'latex' or 'dvipng' - are they installed?"
msgstr ""
#: stats.py:294
msgid "Fact tags"
msgstr "지식 꼬리표"
#: exporting.py:218
msgid "Facts in tab-separated text file (*.txt)"
msgstr "탭으로 구분한 지식 텍스트 파일 (*.txt)"
#: deck.py:1497
msgid "File is in use by another process"
msgstr "다른 프로세스가 파일을 사용하고 있습니다."
#: stats.py:379
#, fuzzy
msgid "First last month"
msgstr "처음"
#: stats.py:376
#, fuzzy
msgid "First last week"
msgstr "처음"
#: stats.py:258
msgid "First review"
msgstr "최초 복습"
#: stats.py:282
msgid "First time"
msgstr "처음"
#: stats.py:346
#, python-format
msgid "First-seen cards: <b>%(gNewYes%)0.1f%%</b> (<b>%(gNewYes)d</b> of <b>%(gNewTotal)d</b>)<br><br>"
msgstr "처음 본 카드: <b>%(gNewYes%)0.1f%%</b> (<b>%(gNewYes)d</b>개 중에 <b>%(gNewTotal)d</b>개)<br><br>"
#: stdmodels.py:110
msgid "From the English expression to the meaning."
msgstr "영어 표현에서 뜻으로"
#: stdmodels.py:142
msgid "From the keyword to the Kanji."
msgstr "중심 의미에서 한자로"
#: stdmodels.py:107
msgid "From the meaning to the English expression."
msgstr "뜻에서 영어 표현으로"
#: stdmodels.py:40
msgid "Front to back"
msgstr "앞면에서 뒷면으로"
#: stdmodels.py:120
msgid "Heisig"
msgstr "Heisig"
#: stats.py:356
#, fuzzy
msgid "Interval"
msgstr "지난 복습 간격"
#: stdmodels.py:51
msgid "Japanese"
msgstr "일본어"
#: stats.py:537
msgid "Jouyou levels:"
msgstr "상용한자 수준"
#: stats.py:271
msgid "Last factor"
msgstr "지난 인수"
#: stats.py:269
msgid "Last interval"
msgstr "지난 복습 간격"
#: models.py:26
msgid "Left"
msgstr "왼쪽"
#: stdmodels.py:183
msgid "Mandarin"
msgstr "북경어"
#: stats.py:286
msgid "Mature"
msgstr "성숙한 카드"
#: graphs.py:210
msgid "Mature cards"
msgstr "성숙한 카드"
#: stats.py:342
#, python-format
msgid "Mature cards: <b>%(gMatureYes%)0.1f%%</b> (<b>%(gMatureYes)d</b> of <b>%(gMatureTotal)d</b>)<br>"
msgstr "성숙한 카드: <b>%(gMatureYes%)0.1f%%</b> (<b>%(gMatureTotal)d</b>개 중에 <b>%(gMatureYes)d</b>개)<br>"
#: stats.py:334
#, python-format
msgid "Mature cards: <b>%(old)d</b> (<b>%(oldP)0.2f%%</b>)<br>"
msgstr "성숙한 카드: <b>%(old)d</b> (<b>%(oldP)0.2f%%</b>)<br>"
#: media.py:161 media.py:163 media.py:164
msgid "Media Missing"
msgstr ""
#: deck.py:915
#, python-format
msgid "Model '%s' has wrong card model count"
msgstr ""
#: deck.py:919
#, python-format
msgid "Model '%s' has wrong field model count"
msgstr ""
#: stats.py:292
msgid "Model tags"
msgstr "모델 꼬리표"
#: graphs.py:208
msgid "New cards"
msgstr "새 카드"
#: stats.py:265
msgid "Next due"
msgstr "복습 예정"
#: stats.py:316
msgid "Please add some cards first.<p/>"
msgstr "카드를 먼저 추가해 주세요.<p/>"
#: stats.py:276
msgid "Repeatedly correct"
msgstr "연속 정답"
#: stats.py:365
#, fuzzy
msgid "Reps last month"
msgstr "%s개월"
#: stats.py:363
msgid "Reps last week"
msgstr ""
#: stats.py:361
#, fuzzy
msgid "Reps next month"
msgstr "%s개월"
#: stats.py:359
msgid "Reps next week"
msgstr ""
#: stats.py:272
msgid "Review count"
msgstr "복습 횟수"
#: models.py:27
msgid "Right"
msgstr "오른쪽"
#: deck.py:1824
#, fuzzy
msgid "Show new cards after all other cards"
msgstr "추가된 순서대로 보여주기"
#: deck.py:1818
msgid "Show new cards in order they were added"
msgstr "추가된 순서대로 보여주기"
#: deck.py:1817
msgid "Show new cards in random order"
msgstr "무작위로 보여주기"
#: deck.py:1823
msgid "Spread new cards out through reviews"
msgstr ""
#: stats.py:287
msgid "State"
msgstr "상태"
#: stdmodels.py:88 stdmodels.py:174 stdmodels.py:203
msgid "Test your ability to recognize the target expression"
msgstr "해당 표현을 이해할 수 있는지 시험해 보세요."
#: exporting.py:145 exporting.py:177
msgid "Text files (*.txt)"
msgstr "텍스트 파일 (*.txt)"
#: stdmodels.py:39
msgid "The answer."
msgstr "답."
#: deck.py:1325
msgid "The current model didn't exist"
msgstr ""
#: deck.py:431
#, fuzzy
msgid "The deck is empty. Please add some cards."
msgstr "카드를 먼저 추가해 주세요.<p/>"
#: deck.py:428
#, python-format
msgid "The next card will be shown in <b>%s</b>"
msgstr ""
#: stats.py:331
#, python-format
msgid "Total number of cards: <b>%d</b><br><br>"
msgstr "전체 카드 개수: <b>%d</b><br><br>"
#: stats.py:279
msgid "Total time"
msgstr "전체 시간"
#: deck.py:702
msgid "Unknown"
msgstr "알 수 없음"
#: stats.py:338
#, python-format
msgid "Unseen cards: <b>%(new)d</b> (<b>%(newP)0.2f%%</b>)<br><br>"
msgstr "시작하지 않은 카드: <b>%(new)d</b> (<b>%(newP)0.2f%%</b>)<br><br>"
#: stats.py:284
msgid "Young"
msgstr "어린 카드"
#: graphs.py:209
msgid "Young cards"
msgstr "어린 카드"
#: stats.py:344
#, python-format
msgid "Young cards: <b>%(gYoungYes%)0.1f%%</b> (<b>%(gYoungYes)d</b> of <b>%(gYoungTotal)d</b>)<br>"
msgstr "어린 카드: <b>%(gYoungYes%)0.1f%%</b> (<b>%(gYoungTotal)d</b>개 중에 <b>%(gYoungYes)d</b>개)<br>"
#: stats.py:336
#, python-format
msgid "Young cards: <b>%(young)d</b> (<b>%(youngP)0.2f%%</b>)<br>"
msgstr "어린 카드: <b>%(young)d</b> (<b>%(youngP)0.2f%%</b>)<br>"
#: models.py:150 models.py:151 models.py:182
msgid "[empty]"
msgstr "[자료 없음]"
#: models.py:148
msgid "[invalid format; see model properties]"
msgstr "[잘못된 형식: 모델 속성 참조]"
#: deck.py:463
msgid "a short time"
msgstr "짧은 시간"
#: stats.py:264
#, python-format
msgid "in %s"
msgstr "%s 안에"
#: deck.py:448
msgid "unknown"
msgstr "알 수 없음"
#~ msgid "Can't read/write directory"
#~ msgstr "디렉토리를 읽고 쓸 수 없음"

638
anki/locale/messages.pot Normal file
View file

@ -0,0 +1,638 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2008-09-23 03:21+0900\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
#: deck.py:483
#, python-format
msgid ""
"\n"
"<h1>Congratulations!</h1>You have finished the deck for now.<br><br>\n"
"%(next)s\n"
"<br><br>\n"
"- There are <b>%(waiting)d</b>\n"
"<a href=\"http://ichi2.net/anki/wiki/Key_Terms_and_Concepts#head-59a81e35b6afb23930005e943068945214d194b3\">\n"
"spaced</a> cards.<br>\n"
"- There are <b>%(suspended)d</b>\n"
"<a href=\"http://ichi2.net/anki/wiki/Key_Terms_and_Concepts#head-37d2db274e6caa23aef55e29655a6b806901774b\">\n"
"suspended</a> cards."
msgstr ""
#: stdmodels.py:121
msgid ""
"\n"
"A format suitable for Heisig's \"Remembering the Kanji\".\n"
"You are tested from the keyword to the kanji.\n"
"\n"
"Layout of the test is based on the great work at\n"
"http://kanji.koohii.com/\n"
"\n"
"The link in the question will list user-contributed\n"
"stories. A free login is required."
msgstr ""
#: stdmodels.py:101
msgid ""
"\n"
"Enter the English expression you want to learn in the 'Expression' field.\n"
"Enter a description in Japanese or English in the 'Meaning' field."
msgstr ""
#: stdmodels.py:52
msgid ""
"\n"
"The reading field is automatically generated by default,\n"
"and shows the reading for the expression. For words that\n"
"are normally written in hiragana or katakana and don't\n"
"need a reading, you can put the word in the expression\n"
"field, and leave the reading field blank. A reading will\n"
"will not automatically be generated for words written\n"
"in only hiragana or katakana.\n"
"\n"
"Note that the automatic generation of meaning is not\n"
"perfect, and should be checked before adding cards."
msgstr ""
#: stats.py:500
#, python-format
msgid "%(count)s %(gradename)s kanji."
msgstr ""
#: stats.py:498
#, python-format
msgid "%(gradename)s: %(count)s of %(total)s (%(percent)0.1f%%)."
msgstr ""
#: stats.py:277 stats.py:279
#, python-format
msgid "%0.1f seconds"
msgstr ""
#: stats.py:262 stats.py:303
#, python-format
msgid "%s ago"
msgstr ""
#: utils.py:19
#, python-format
msgid "%s day"
msgid_plural "%s days"
msgstr[0] ""
msgstr[1] ""
#: utils.py:20
#, python-format
msgid "%s hour"
msgid_plural "%s hours"
msgstr[0] ""
msgstr[1] ""
#: utils.py:21
#, python-format
msgid "%s minute"
msgid_plural "%s minutes"
msgstr[0] ""
msgstr[1] ""
#: utils.py:18
#, python-format
msgid "%s month"
msgid_plural "%s months"
msgstr[0] ""
msgstr[1] ""
#: utils.py:22
#, python-format
msgid "%s second"
msgid_plural "%s seconds"
msgstr[0] ""
msgstr[1] ""
#: utils.py:17
#, python-format
msgid "%s year"
msgid_plural "%s years"
msgstr[0] ""
msgstr[1] ""
#: stats.py:370 stats.py:373 stats.py:376 stats.py:379
#, python-format
msgid "<b>%(a)d</b> (<b>%(b)0.1f</b>/day)"
msgstr ""
#: stats.py:367
#, python-format
msgid "<b>%(a)d</b>/day, <b>%(b)d</b>/mon"
msgstr ""
#: stats.py:356
#, python-format
msgid "<b>%0.0f</b> days"
msgstr ""
#: stats.py:357 stats.py:359 stats.py:361 stats.py:363 stats.py:365
#, python-format
msgid "<b>%0.1f</b> cards/day"
msgstr ""
#: stats.py:354
msgid "<b>Averages</b><br>"
msgstr ""
#: stats.py:333
msgid "<b>Card counts</b><br>"
msgstr ""
#: stats.py:341
msgid "<b>Correct answers</b><br>"
msgstr ""
#: stats.py:522
#, python-format
msgid "<h1>Kanji statistics</h1>The %d seen cards in this deck contain:"
msgstr ""
#: stats.py:526
#, python-format
msgid "<li>%d total unique kanji.</li>"
msgstr ""
#: sync.py:171
#, python-format
msgid ""
"<table width=500>\n"
"<tr><td><b>Added/Changed</b></td><td><b>Here</b></td><td><b>Server</b></td></tr>\n"
"<tr><td>Cards</td><td>%(lc)d</td><td>%(rc)d</td></tr>\n"
"<tr><td>Facts</td><td>%(lf)d</td><td>%(rf)d</td></tr>\n"
"<tr><td>Models</td><td>%(lm)d</td><td>%(rm)d</td></tr>\n"
"%(media)s\n"
"</table>"
msgstr ""
#: stdmodels.py:32
msgid ""
"A basic flashcard with a front and a back.\n"
"Questions are asked from front to back by default.\n"
"\n"
"Please consider customizing this model, rather than\n"
"using it verbatim: field names like \"expression\" are\n"
"clearer than \"front\" and \"back\", and will ensure\n"
"that your entries are consistent."
msgstr ""
#: stdmodels.py:164
msgid "A description in your native language, or Cantonese"
msgstr ""
#: stdmodels.py:75
msgid "A description in your native language, or Japanese"
msgstr ""
#: stdmodels.py:192
msgid "A description in your native language, or Mandarin"
msgstr ""
#: stdmodels.py:38
msgid "A question."
msgstr ""
#: stdmodels.py:159 stdmodels.py:186
msgid "A word or expression written in Hanzi."
msgstr ""
#: stdmodels.py:65
msgid "A word or expression written in Kanji."
msgstr ""
#: stdmodels.py:84 stdmodels.py:170 stdmodels.py:199
msgid "Actively test your recall by producing the target expression"
msgstr ""
#: stats.py:256
msgid "Added"
msgstr ""
#: stats.py:373
msgid "Added last month"
msgstr ""
#: stats.py:370
msgid "Added last week"
msgstr ""
#: exporting.py:216
msgid "Anki deck (*.anki)"
msgstr ""
#: exporting.py:55
msgid "Anki decks (*.anki)"
msgstr ""
#: deck.py:418
#, python-format
msgid ""
"At the same time tomorrow:<br><br>\n"
"- There will be <b>%(wait)d</b> cards waiting for review<br>\n"
"- There will be <b>%(new)d</b>\n"
"<a href=\"http://ichi2.net/anki/wiki/Key_Terms_and_Concepts#head-061e5433d4571d7ec7ecba0c329c09bd27c84d63\">\n"
"new cards</a> waiting"
msgstr ""
#: stdmodels.py:78 stdmodels.py:166 stdmodels.py:194
msgid "Automatically generated by default."
msgstr ""
#: stats.py:357
msgid "Average reps"
msgstr ""
#: stats.py:277
msgid "Average time"
msgstr ""
#: stats.py:367
msgid "Avg. added"
msgstr ""
#: stdmodels.py:42
msgid "Back to front"
msgstr ""
#: stdmodels.py:31
msgid "Basic"
msgstr ""
#: deck.py:1452 deck.py:1525
msgid "Can't read/write deck"
msgstr ""
#: stdmodels.py:156
msgid "Cantonese"
msgstr ""
#: stats.py:290
msgid "Card model tags"
msgstr ""
#: stats.py:289
msgid "Card tags"
msgstr ""
#: graphs.py:77 graphs.py:99 graphs.py:113 graphs.py:135
msgid "Cards"
msgstr ""
#: exporting.py:217
msgid "Cards in tab-separated text file (*.txt)"
msgstr ""
#: models.py:25
msgid "Center"
msgstr ""
#: stats.py:259
msgid "Changed"
msgstr ""
#: stats.py:274
msgid "Correct count"
msgstr ""
#: stats.py:270
msgid "Current factor"
msgstr ""
#: stats.py:266
msgid "Current interval"
msgstr ""
#: deck.py:1311
msgid "Database file damaged. Restore from backup."
msgstr ""
#: graphs.py:136
msgid "Day"
msgstr ""
#: graphs.py:78 graphs.py:100 graphs.py:114
msgid "Days"
msgstr ""
#: stats.py:318
msgid "Deck Statistics"
msgstr ""
#: stats.py:319
#, python-format
msgid "Deck created: <b>%s</b> ago<br>"
msgstr ""
#: deck.py:1319
msgid "Deck was missing a model"
msgstr ""
#: deck.py:1340
#, python-format
msgid "Deleted %d cards with missing fact"
msgstr ""
#: deck.py:1348
#, python-format
msgid "Deleted %d cards with no card model"
msgstr ""
#: deck.py:1361
#, python-format
msgid "Deleted %d dangling fields"
msgstr ""
#: deck.py:1333
#, python-format
msgid "Deleted %d facts with missing fields"
msgstr ""
#: deck.py:1353
#, python-format
msgid "Deleted %d facts with no cards"
msgstr ""
#: stdmodels.py:100
msgid "English"
msgstr ""
#: latex.py:104
msgid "Error executing 'latex' or 'dvipng' - are they installed?"
msgstr ""
#: stats.py:294
msgid "Fact tags"
msgstr ""
#: exporting.py:218
msgid "Facts in tab-separated text file (*.txt)"
msgstr ""
#: deck.py:1497
msgid "File is in use by another process"
msgstr ""
#: stats.py:379
msgid "First last month"
msgstr ""
#: stats.py:376
msgid "First last week"
msgstr ""
#: stats.py:258
msgid "First review"
msgstr ""
#: stats.py:282
msgid "First time"
msgstr ""
#: stats.py:346
#, python-format
msgid "First-seen cards: <b>%(gNewYes%)0.1f%%</b> (<b>%(gNewYes)d</b> of <b>%(gNewTotal)d</b>)<br><br>"
msgstr ""
#: stdmodels.py:110
msgid "From the English expression to the meaning."
msgstr ""
#: stdmodels.py:142
msgid "From the keyword to the Kanji."
msgstr ""
#: stdmodels.py:107
msgid "From the meaning to the English expression."
msgstr ""
#: stdmodels.py:40
msgid "Front to back"
msgstr ""
#: stdmodels.py:120
msgid "Heisig"
msgstr ""
#: stats.py:356
msgid "Interval"
msgstr ""
#: stdmodels.py:51
msgid "Japanese"
msgstr ""
#: stats.py:537
msgid "Jouyou levels:"
msgstr ""
#: stats.py:271
msgid "Last factor"
msgstr ""
#: stats.py:269
msgid "Last interval"
msgstr ""
#: models.py:26
msgid "Left"
msgstr ""
#: stdmodels.py:183
msgid "Mandarin"
msgstr ""
#: stats.py:286
msgid "Mature"
msgstr ""
#: graphs.py:210
msgid "Mature cards"
msgstr ""
#: stats.py:342
#, python-format
msgid "Mature cards: <b>%(gMatureYes%)0.1f%%</b> (<b>%(gMatureYes)d</b> of <b>%(gMatureTotal)d</b>)<br>"
msgstr ""
#: stats.py:334
#, python-format
msgid "Mature cards: <b>%(old)d</b> (<b>%(oldP)0.2f%%</b>)<br>"
msgstr ""
#: media.py:161 media.py:163 media.py:164
msgid "Media Missing"
msgstr ""
#: deck.py:915
#, python-format
msgid "Model '%s' has wrong card model count"
msgstr ""
#: deck.py:919
#, python-format
msgid "Model '%s' has wrong field model count"
msgstr ""
#: stats.py:292
msgid "Model tags"
msgstr ""
#: graphs.py:208
msgid "New cards"
msgstr ""
#: stats.py:265
msgid "Next due"
msgstr ""
#: stats.py:316
msgid "Please add some cards first.<p/>"
msgstr ""
#: stats.py:276
msgid "Repeatedly correct"
msgstr ""
#: stats.py:365
msgid "Reps last month"
msgstr ""
#: stats.py:363
msgid "Reps last week"
msgstr ""
#: stats.py:361
msgid "Reps next month"
msgstr ""
#: stats.py:359
msgid "Reps next week"
msgstr ""
#: stats.py:272
msgid "Review count"
msgstr ""
#: models.py:27
msgid "Right"
msgstr ""
#: deck.py:1824
msgid "Show new cards after all other cards"
msgstr ""
#: deck.py:1818
msgid "Show new cards in order they were added"
msgstr ""
#: deck.py:1817
msgid "Show new cards in random order"
msgstr ""
#: deck.py:1823
msgid "Spread new cards out through reviews"
msgstr ""
#: stats.py:287
msgid "State"
msgstr ""
#: stdmodels.py:88 stdmodels.py:174 stdmodels.py:203
msgid "Test your ability to recognize the target expression"
msgstr ""
#: exporting.py:145 exporting.py:177
msgid "Text files (*.txt)"
msgstr ""
#: stdmodels.py:39
msgid "The answer."
msgstr ""
#: deck.py:1325
msgid "The current model didn't exist"
msgstr ""
#: deck.py:431
msgid "The deck is empty. Please add some cards."
msgstr ""
#: deck.py:428
#, python-format
msgid "The next card will be shown in <b>%s</b>"
msgstr ""
#: stats.py:331
#, python-format
msgid "Total number of cards: <b>%d</b><br><br>"
msgstr ""
#: stats.py:279
msgid "Total time"
msgstr ""
#: deck.py:702
msgid "Unknown"
msgstr ""
#: stats.py:338
#, python-format
msgid "Unseen cards: <b>%(new)d</b> (<b>%(newP)0.2f%%</b>)<br><br>"
msgstr ""
#: stats.py:284
msgid "Young"
msgstr ""
#: graphs.py:209
msgid "Young cards"
msgstr ""
#: stats.py:344
#, python-format
msgid "Young cards: <b>%(gYoungYes%)0.1f%%</b> (<b>%(gYoungYes)d</b> of <b>%(gYoungTotal)d</b>)<br>"
msgstr ""
#: stats.py:336
#, python-format
msgid "Young cards: <b>%(young)d</b> (<b>%(youngP)0.2f%%</b>)<br>"
msgstr ""
#: models.py:150 models.py:151 models.py:182
msgid "[empty]"
msgstr ""
#: models.py:148
msgid "[invalid format; see model properties]"
msgstr ""
#: deck.py:463
msgid "a short time"
msgstr ""
#: stats.py:264
#, python-format
msgid "in %s"
msgstr ""
#: deck.py:448
msgid "unknown"
msgstr ""

201
anki/media.py Normal file
View file

@ -0,0 +1,201 @@
# -*- coding: utf-8 -*-
# Copyright: Damien Elmes <anki@ichi2.net>
# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html
"""\
Media support
====================
"""
__docformat__ = 'restructuredtext'
try:
import hashlib
md5 = hashlib.md5
except ImportError:
import md5
md5 = md5.new
import os, stat, time, shutil, re
from anki.db import *
from anki.facts import Fact
from anki.utils import addTags, genID, ids2str
from anki.lang import _
regexps = (("(\[sound:([^]]+)\])",
"[sound:%s]"),
("(<img src=[\"']?([^\"'>]+)[\"']? ?/?>)",
"<img src=\"%s\">"))
# Tables
##########################################################################
mediaTable = Table(
'media', metadata,
Column('id', Integer, primary_key=True, nullable=False),
Column('filename', UnicodeText, nullable=False),
Column('size', Integer, nullable=False),
Column('created', Float, nullable=False),
Column('originalPath', UnicodeText, nullable=False, default=u""),
Column('description', UnicodeText, nullable=False, default=u""))
class Media(object):
pass
mapper(Media, mediaTable)
mediaDeletedTable = Table(
'mediaDeleted', metadata,
Column('mediaId', Integer, ForeignKey("cards.id"),
nullable=False),
Column('deletedTime', Float, nullable=False))
# Helper functions
##########################################################################
def checksum(data):
return md5(data).hexdigest()
def mediaFilename(path):
"Return checksum.ext for path"
new = checksum(open(path, "rb").read())
ext = os.path.splitext(path)[1]
return "%s%s" % (new, ext)
def copyToMedia(deck, path):
"""Copy PATH to MEDIADIR, and return new filename.
Update media table. If file already exists, don't copy."""
newBase = mediaFilename(path)
new = os.path.join(deck.mediaDir(create=True), newBase)
# copy if not existing
if not os.path.exists(new):
shutil.copy2(path, new)
newSize = os.stat(new)[stat.ST_SIZE]
if not deck.s.scalar(
"select 1 from media where filename = :f",
f=newBase):
deck.s.statement("""
insert into media (id, filename, size, created, originalPath,
description)
values (:id, :filename, :size, :created, :originalPath,
:description)""",
id=genID(),
filename=newBase,
size=newSize,
created=time.time(),
originalPath=path,
description=os.path.splitext(
os.path.basename(path))[0])
return newBase
def _modifyFields(deck, fieldsToUpdate, modifiedFacts, dirty):
factIds = ids2str(modifiedFacts.keys())
if fieldsToUpdate:
deck.s.execute("update fields set value = :val where id = :id",
fieldsToUpdate)
deck.s.statement(
"update facts set modified = :time where id in %s" %
factIds, time=time.time())
ids = deck.s.all("""select cards.id, cards.cardModelId, facts.id
from cards, facts where cards.factId = facts.id and facts.id in %s"""
% factIds)
deck.updateCardQACache(ids, dirty)
deck.flushMod()
def rebuildMediaDir(deck, deleteRefs=False, dirty=True):
"Delete references to missing files, delete unused files."
localFiles = {}
modifiedFacts = {}
unmodifiedFacts = {}
renamedFiles = {}
existingFiles = {}
factsMissingMedia = {}
updateFields = []
usedFiles = {}
unusedFileCount = 0
missingFileCount = 0
deck.mediaDir(create=True)
# rename all files to checksum versions, note non-renamed ones
for oldBase in os.listdir(unicode(deck.mediaDir())):
oldPath = os.path.join(deck.mediaDir(), oldBase)
if oldBase.startswith("."):
continue
if oldBase.startswith("latex-"):
continue
if os.path.isdir(oldPath):
continue
newBase = copyToMedia(deck, oldPath)
if oldBase == newBase:
existingFiles[oldBase] = 1
else:
renamedFiles[oldBase] = newBase
# now look through all fields, and update references to files
for (id, fid, val) in deck.s.all(
"select id, factId, value from fields"):
oldval = val
for (full, fname, repl) in mediaRefs(val):
if fname in renamedFiles:
# renamed
newBase = renamedFiles[fname]
val = re.sub(re.escape(full), repl % newBase, val)
usedFiles[newBase] = 1
elif fname in existingFiles:
# used & current
usedFiles[fname] = 1
else:
# missing
missingFileCount += 1
if deleteRefs:
val = re.sub(re.escape(full), "", val)
else:
factsMissingMedia[fid] = 1
if val != oldval:
updateFields.append({'id': id, 'val': val})
modifiedFacts[fid] = 1
else:
unmodifiedFacts[fid] = 1
# update modified fields
if modifiedFacts:
_modifyFields(deck, updateFields, modifiedFacts, dirty)
# fix tags
if dirty:
if deleteRefs:
deck.deleteFactTags(modifiedFacts.keys(), _("Media Missing"))
else:
deck.addFactTags(modifiedFacts.keys(), _("Media Missing"))
deck.deleteFactTags(unmodifiedFacts.keys(), _("Media Missing"))
# build cache of db records
mediaIds = dict(deck.s.all("select filename, id from media"))
# look through the media dir for any unused files, and delete
for f in os.listdir(unicode(deck.mediaDir())):
if f.startswith("."):
continue
if f.startswith("latex-"):
continue
path = os.path.join(deck.mediaDir(), f)
if os.path.isdir(path):
shutil.rmtree(path)
continue
if f in usedFiles:
del mediaIds[f]
else:
os.unlink(path)
unusedFileCount += 1
for (fname, id) in mediaIds.items():
# maybe delete from db
if id:
deck.s.statement("delete from media where id = :id", id=id)
deck.s.statement("""
insert into mediaDeleted (mediaId, deletedTime)
values (:id, strftime('%s', 'now'))""", id=id)
# update deck and save
deck.flushMod()
deck.save()
return missingFileCount, unusedFileCount
def mediaRefs(string):
"Return list of (fullMatch, filename, replacementString)."
l = []
for (reg, repl) in regexps:
for (full, fname) in re.findall(reg, string):
l.append((full, fname, repl))
return l

255
anki/models.py Normal file
View file

@ -0,0 +1,255 @@
# -*- coding: utf-8 -*-
# Copyright: Damien Elmes <anki@ichi2.net>
# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html
"""\
Model - define the way in which facts are added and shown
==========================================================
- Field models
- Card models
- Models
"""
import time
from sqlalchemy.ext.orderinglist import ordering_list
from anki.db import *
from anki.utils import genID
from anki.fonts import toPlatformFont
from anki.utils import parseTags
from anki.lang import _
def alignmentLabels():
return {
0: _("Center"),
1: _("Left"),
2: _("Right"),
}
# Field models
##########################################################################
fieldModelsTable = Table(
'fieldModels', metadata,
Column('id', Integer, primary_key=True),
Column('ordinal', Integer, nullable=False),
Column('modelId', Integer, ForeignKey('models.id'), nullable=False),
Column('name', UnicodeText, nullable=False),
Column('description', UnicodeText, nullable=False, default=u""),
Column('features', UnicodeText, nullable=False, default=u""),
Column('required', Boolean, nullable=False, default=True),
Column('unique', Boolean, nullable=False, default=True),
Column('numeric', Boolean, nullable=False, default=False),
# display
Column('quizFontFamily', UnicodeText),
Column('quizFontSize', Integer),
Column('quizFontColour', String(7)),
Column('editFontFamily', UnicodeText),
Column('editFontSize', Integer, default=20))
class FieldModel(object):
"The definition of one field in a fact."
def __init__(self, name=u"", description=u"", required=True, unique=True):
self.name = name
self.description = description
self.required = required
self.unique = unique
self.id = genID()
def css(self, type="quiz"):
t = ".%s { " % self.name.replace(" ", "")
if getattr(self, type+'FontFamily'):
t += "font-family: \"%s\"; " % toPlatformFont(
getattr(self, type+'FontFamily'))
if getattr(self, type+'FontSize'):
t += "font-size: %dpx; " % getattr(self, type+'FontSize')
if type == "quiz" and getattr(self, type+'FontColour'):
t += "color: %s; " % getattr(self, type+'FontColour')
t += " }\n"
return t
mapper(FieldModel, fieldModelsTable)
# Card models
##########################################################################
cardModelsTable = Table(
'cardModels', metadata,
Column('id', Integer, primary_key=True),
Column('ordinal', Integer, nullable=False),
Column('modelId', Integer, ForeignKey('models.id'), nullable=False),
Column('name', UnicodeText, nullable=False),
Column('description', UnicodeText, nullable=False, default=u""),
Column('active', Boolean, nullable=False, default=True),
# formats: question/answer/last(not used)
Column('qformat', UnicodeText, nullable=False),
Column('aformat', UnicodeText, nullable=False),
Column('lformat', UnicodeText),
# question/answer editor format (not used yet)
Column('qedformat', UnicodeText),
Column('aedformat', UnicodeText),
Column('questionInAnswer', Boolean, nullable=False, default=False),
# display
Column('questionFontFamily', UnicodeText, default=u"Arial"),
Column('questionFontSize', Integer, default=20),
Column('questionFontColour', String(7), default=u"#000000"),
Column('questionAlign', Integer, default=0),
Column('answerFontFamily', UnicodeText, default=u"Arial"),
Column('answerFontSize', Integer, default=20),
Column('answerFontColour', String(7), default=u"#000000"),
Column('answerAlign', Integer, default=0),
Column('lastFontFamily', UnicodeText, default=u"Arial"),
Column('lastFontSize', Integer, default=20),
Column('lastFontColour', String(7), default=u"#000000"),
Column('editQuestionFontFamily', UnicodeText, default=None),
Column('editQuestionFontSize', Integer, default=None),
Column('editAnswerFontFamily', UnicodeText, default=None),
Column('editAnswerFontSize', Integer, default=None))
class CardModel(object):
"""Represents how to generate the front and back of a card."""
def __init__(self, name=u"", description=u"",
qformat=u"q", aformat=u"a", active=True):
self.name = name
self.description = description
self.qformat = qformat
self.aformat = aformat
self.active = active
self.id = genID()
def renderQA(self, card, fact, type, format="text"):
"Render fact into card based on card model."
if type == "question": field = self.qformat
elif type == "answer": field = self.aformat
htmlFields = {}
htmlFields.update(fact)
alltags = parseTags(card.tags + "," +
card.fact.tags + "," +
card.cardModel.name + "," +
card.fact.model.tags)
htmlFields['tags'] = ", ".join(alltags)
textFields = {}
textFields.update(htmlFields)
# add per-field formatting
for (k, v) in htmlFields.items():
# generate pure text entries
htmlFields["text:"+k] = v
textFields["text:"+k] = v
if v:
# convert newlines to html & add spans to fields
v = v.replace("\n", "<br>")
htmlFields[k] = '<span class="%s">%s</span>' % (k.replace(" ",""), v)
try:
html = field % htmlFields
text = field % textFields
except (KeyError, TypeError, ValueError):
return _("[invalid format; see model properties]")
if not html:
html = _("[empty]")
text = _("[empty]")
if format == "text":
return text
# add outer div & alignment (with tables due to qt's html handling)
html = '<div class="%s">%s</div>' % (type, html)
attr = type + 'Align'
if getattr(self, attr) == 0:
align = "center"
elif getattr(self, attr) == 1:
align = "left"
else:
align = "right"
html = (("<center><table width=95%%><tr><td align=%s>" % align) +
html + "</td></tr></table></center>")
return html
def renderQASQL(self, type, factId):
"Render QA in pure SQL, with no HTML generation."
fields = dict(object_session(self).all("""
select fieldModels.name, fields.value from fields, fieldModels
where
fields.factId = :factId and
fields.fieldModelId = fieldModels.id""", factId=factId))
fields['tags'] = u""
for (k, v) in fields.items():
fields["text:"+k] = v
if type == "q": format = self.qformat
else: format = self.aformat
try:
return format % fields
except (KeyError, TypeError, ValueError):
return _("[empty]")
def css(self):
"Return the CSS markup for this card."
t = ""
for type in ("question", "answer"):
t += ".%s { font-family: \"%s\"; color: %s; font-size: %dpx; }\n" % (
type,
toPlatformFont(getattr(self, type+"FontFamily")),
getattr(self, type+"FontColour"),
getattr(self, type+"FontSize"))
return t
mapper(CardModel, cardModelsTable)
# Model table
##########################################################################
modelsTable = Table(
'models', metadata,
Column('id', Integer, primary_key=True),
Column('deckId', Integer, ForeignKey("decks.id", use_alter=True, name="deckIdfk")),
Column('created', Float, nullable=False, default=time.time),
Column('modified', Float, nullable=False, default=time.time),
Column('tags', UnicodeText, nullable=False, default=u""),
Column('name', UnicodeText, nullable=False),
Column('description', UnicodeText, nullable=False, default=u""),
Column('features', UnicodeText, nullable=False, default=u""),
Column('spacing', Float, nullable=False, default=0.1),
Column('initialSpacing', Float, nullable=False, default=600))
class Model(object):
"Defines the way a fact behaves, what fields it can contain, etc."
def __init__(self, name=u"", description=u""):
self.name = name
self.description = description
self.id = genID()
def setModified(self):
self.modified = time.time()
def addFieldModel(self, field):
"Add a field model."
self.fieldModels.append(field)
s = object_session(self)
if s:
s.flush()
def addCardModel(self, card):
"Add a card model."
self.cardModels.append(card)
s = object_session(self)
if s:
s.flush()
mapper(Model, modelsTable, properties={
'fieldModels': relation(FieldModel, backref='model',
collection_class=ordering_list('ordinal'),
order_by=[fieldModelsTable.c.ordinal],
cascade="all, delete-orphan"),
'cardModels': relation(CardModel, backref='model',
collection_class=ordering_list('ordinal'),
order_by=[cardModelsTable.c.ordinal],
cascade="all, delete-orphan"),
})
# Model deletions
##########################################################################
modelsDeletedTable = Table(
'modelsDeleted', metadata,
Column('modelId', Integer, ForeignKey("models.id"),
nullable=False),
Column('deletedTime', Float, nullable=False))

95
anki/sound.py Normal file
View file

@ -0,0 +1,95 @@
# -*- coding: utf-8 -*-
# Copyright: Damien Elmes <anki@ichi2.net>
# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html
"""\
Sound support
==============================
"""
__docformat__ = 'restructuredtext'
import re, sys
try:
import pygame
pygame.mixer.pre_init(44100,-16,2, 1024 * 3)
pygame.mixer.init()
soundsAvailable = True
except:
soundsAvailable = False
print "Warning, pygame not available. No sounds will play."
def playPyGame(path):
"Play a sound. Expects a unicode pathname."
if not soundsAvailable:
return
path = path.encode(sys.getfilesystemencoding())
try:
if pygame.mixer.music.get_busy():
pygame.mixer.music.queue(path)
else:
pygame.mixer.music.load(path)
pygame.mixer.music.play()
except pygame.error:
return
def playFromText(text):
for match in re.findall("\[sound:(.*?)\]", text):
play(match)
def stripSounds(text):
return re.sub("\[sound:.*?\]", "", text)
def hasSound(text):
return re.search("\[sound:.*?\]", text) is not None
# Mac audio support
##########################################################################
try:
from AppKit import NSSound, NSObject
queue = []
current = None
class Sound(NSObject):
def init(self):
return self
def sound_didFinishPlaying_(self, sound, bool):
global current
while 1:
if not queue:
break
next = queue.pop(0)
if play_(next):
break
s = Sound.new()
def playOSX(path):
global current
if current:
if current.isPlaying():
queue.append(path)
return
# new handle
play_(path)
def play_(path):
global current
current = NSSound.alloc()
current = current.initWithContentsOfFile_byReference_(path, True)
if not current:
return False
current.setDelegate_(s)
current.play()
return True
except ImportError:
pass
if sys.platform.startswith("darwin"):
play = playOSX
else:
play = playPyGame

581
anki/stats.py Normal file
View file

@ -0,0 +1,581 @@
# -*- coding: utf-8 -*-
# Copyright: Damien Elmes <anki@ichi2.net>
# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html
"""\
Statistical tracking and reports
=================================
"""
__docformat__ = 'restructuredtext'
# we track statistics over the life of the deck, and per-day
STATS_LIFE = 0
STATS_DAY = 1
import unicodedata, time, sys, os, datetime
import anki, anki.utils
from datetime import date
from anki.db import *
from anki.lang import _
# Tracking stats on the DB
##########################################################################
statsTable = Table(
'stats', metadata,
Column('id', Integer, primary_key=True),
Column('type', Integer, nullable=False),
Column('day', Date, nullable=False, default=date.today),
Column('reps', Integer, nullable=False, default=0),
Column('averageTime', Float, nullable=False, default=0),
Column('reviewTime', Float, nullable=False, default=0),
# next two columns no longer used
Column('distractedTime', Float, nullable=False, default=0),
Column('distractedReps', Integer, nullable=False, default=0),
Column('newEase0', Integer, nullable=False, default=0),
Column('newEase1', Integer, nullable=False, default=0),
Column('newEase2', Integer, nullable=False, default=0),
Column('newEase3', Integer, nullable=False, default=0),
Column('newEase4', Integer, nullable=False, default=0),
Column('youngEase0', Integer, nullable=False, default=0),
Column('youngEase1', Integer, nullable=False, default=0),
Column('youngEase2', Integer, nullable=False, default=0),
Column('youngEase3', Integer, nullable=False, default=0),
Column('youngEase4', Integer, nullable=False, default=0),
Column('matureEase0', Integer, nullable=False, default=0),
Column('matureEase1', Integer, nullable=False, default=0),
Column('matureEase2', Integer, nullable=False, default=0),
Column('matureEase3', Integer, nullable=False, default=0),
Column('matureEase4', Integer, nullable=False, default=0))
class Stats(object):
def __init__(self):
self.day = date.today()
self.reps = 0
self.averageTime = 0
self.reviewTime = 0
self.distractedTime = 0
self.distractedReps = 0
self.newEase0 = 0
self.newEase1 = 0
self.newEase2 = 0
self.newEase3 = 0
self.newEase4 = 0
self.youngEase0 = 0
self.youngEase1 = 0
self.youngEase2 = 0
self.youngEase3 = 0
self.youngEase4 = 0
self.matureEase0 = 0
self.matureEase1 = 0
self.matureEase2 = 0
self.matureEase3 = 0
self.matureEase4 = 0
def fromDB(self, s, id):
r = s.first("select * from stats where id = :id", id=id)
(self.id,
self.type,
self.day,
self.reps,
self.averageTime,
self.reviewTime,
self.distractedTime,
self.distractedReps,
self.newEase0,
self.newEase1,
self.newEase2,
self.newEase3,
self.newEase4,
self.youngEase0,
self.youngEase1,
self.youngEase2,
self.youngEase3,
self.youngEase4,
self.matureEase0,
self.matureEase1,
self.matureEase2,
self.matureEase3,
self.matureEase4) = r
self.day = datetime.date(*[int(i) for i in self.day.split("-")])
def create(self, s, type, day):
self.type = type
self.day = day
s.execute("""insert into stats
(type, day, reps, averageTime, reviewTime, distractedTime, distractedReps,
newEase0, newEase1, newEase2, newEase3, newEase4, youngEase0, youngEase1,
youngEase2, youngEase3, youngEase4, matureEase0, matureEase1, matureEase2,
matureEase3, matureEase4) values (:type, :day, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)""", self.__dict__)
self.id = s.scalar(
"select id from stats where type = :type and day = :day",
type=type, day=day)
def toDB(self, s):
assert self.id
s.execute("""update stats set
type=:type,
day=:day,
reps=:reps,
averageTime=:averageTime,
reviewTime=:reviewTime,
newEase0=:newEase0,
newEase1=:newEase1,
newEase2=:newEase2,
newEase3=:newEase3,
newEase4=:newEase4,
youngEase0=:youngEase0,
youngEase1=:youngEase1,
youngEase2=:youngEase2,
youngEase3=:youngEase3,
youngEase4=:youngEase4,
matureEase0=:matureEase0,
matureEase1=:matureEase1,
matureEase2=:matureEase2,
matureEase3=:matureEase3,
matureEase4=:matureEase4
where id = :id""", self.__dict__)
mapper(Stats, statsTable)
def updateAllStats(s, gs, ds, card, ease, oldState):
"Update global and daily statistics."
updateStats(s, gs, card, ease, oldState)
updateStats(s, ds, card, ease, oldState)
def updateStats(s, stats, card, ease, oldState):
stats.reps += 1
delay = card.thinkingTime()
if delay >= 60:
# make a guess as to the time spent answering
stats.reviewTime += stats.averageTime
else:
stats.reviewTime += delay
stats.averageTime = (
stats.reviewTime / float(stats.reps))
# update eases
attr = oldState + "Ease%d" % ease
setattr(stats, attr, getattr(stats, attr) + 1)
stats.toDB(s)
def globalStats(s):
type = STATS_LIFE
today = date.today()
id = s.scalar("select id from stats where type = :type",
type=type)
stats = Stats()
if id:
stats.fromDB(s, id)
return stats
else:
stats.create(s, type, today)
stats.type = type
return stats
def dailyStats(s):
type = STATS_DAY
today = date.today()
id = s.scalar("select id from stats where type = :type and day = :day",
type=type, day=today)
stats = Stats()
if id:
stats.fromDB(s, id)
return stats
else:
stats.create(s, type, today)
return stats
def summarizeStats(stats, pre=""):
"Generate percentages and total counts for STATS. Optionally prefix."
cardTypes = ("new", "young", "mature")
h = {}
# total counts
###############
for type in cardTypes:
# total yes/no for type, eg. gNewYes
h[pre + type.capitalize() + "No"] = (getattr(stats, type + "Ease0") +
getattr(stats, type + "Ease1"))
h[pre + type.capitalize() + "Yes"] = (getattr(stats, type + "Ease2") +
getattr(stats, type + "Ease3") +
getattr(stats, type + "Ease4"))
# total for type, eg. gNewTotal
h[pre + type.capitalize() + "Total"] = (
h[pre + type.capitalize() + "No"] +
h[pre + type.capitalize() + "Yes"])
# total yes/no, eg. gYesTotal
for answer in ("yes", "no"):
num = 0
for type in cardTypes:
num += h[pre + type.capitalize() + answer.capitalize()]
h[pre + answer.capitalize() + "Total"] = num
# total over all, eg. gTotal
num = 0
for type in cardTypes:
num += h[pre + type.capitalize() + "Total"]
h[pre + "Total"] = num
# percentages
##############
for type in cardTypes:
# total yes/no % by type, eg. gNewYes%
for answer in ("yes", "no"):
setPercentage(h, pre + type.capitalize() + answer.capitalize(),
pre + type.capitalize())
for answer in ("yes", "no"):
# total yes/no, eg. gYesTotal%
setPercentage(h, pre + answer.capitalize() + "Total", pre)
h[pre + 'AverageTime'] = stats.averageTime
h[pre + 'ReviewTime'] = stats.reviewTime
return h
def setPercentage(h, a, b):
try:
h[a + "%"] = (h[a] / float(h[b + "Total"])) * 100
except ZeroDivisionError:
h[a + "%"] = 0
def getStats(s, gs, ds):
"Return a handy dictionary exposing a number of internal stats."
h = {}
h.update(summarizeStats(gs, "g"))
h.update(summarizeStats(ds, "d"))
return h
# Card stats
##########################################################################
class CardStats(object):
def __init__(self, deck, card):
self.deck = deck
self.card = card
def report(self):
c = self.card
self.txt = "<table width=250>"
self.addLine(_("Added"), self.strTime(c.created))
if c.firstAnswered:
self.addLine(_("First review"), self.strTime(c.firstAnswered))
self.addLine(_("Changed"), self.strTime(c.modified))
next = time.time() - c.due
if next > 0:
next = _("%s ago") % anki.utils.fmtTimeSpan(next)
else:
next = _("in %s") % anki.utils.fmtTimeSpan(abs(next))
self.addLine(_("Next due"), next)
self.addLine(_("Current interval"), "%0.2f days" % c.interval)
if c.interval != c.lastInterval:
# don't show the last interval if it hasn't been updated yet
self.addLine(_("Last interval"), "%0.2f days" % c.lastInterval)
self.addLine(_("Current factor"), "%0.2f" % c.factor)
self.addLine(_("Last factor"), "%0.2f" % c.lastFactor)
self.addLine(_("Review count"), c.reps)
if c.reps:
self.addLine(_("Correct count"), "%d (%0.2f%%)" % (
c.yesCount, (c.yesCount / float(c.reps))*100))
self.addLine(_("Repeatedly correct"), c.successive)
self.addLine(_("Average time"), _("%0.1f seconds") %
c.averageTime)
self.addLine(_("Total time"), _("%0.1f seconds") %
c.reviewTime)
if self.deck.cardIsNew(c):
state = _("First time")
elif self.deck.cardIsBeingLearnt(c):
state = _("Young")
else:
state = _("Mature")
self.addLine(_("State"), state)
if c.tags:
self.addLine(_("Card tags"), c.tags)
self.addLine(_("Card model tags"), c.cardModel.name)
if c.fact.model.tags:
self.addLine(_("Model tags"), c.fact.model.tags)
if c.fact.tags:
self.addLine(_("Fact tags"), c.fact.tags)
self.txt += "</table>"
return self.txt
def addLine(self, k, v):
self.txt += "<tr><td><b>%s<b></td><td>%s</td></tr>" % (k, v)
def strTime(self, tm):
s = anki.utils.fmtTimeSpan(time.time() - tm)
return _("%s ago") % s
# Deck stats (specific to the 'sched' scheduler)
##########################################################################
class DeckStats(object):
def __init__(self, deck):
self.deck = deck
def report(self):
"Return an HTML string with a report."
if self.deck.isEmpty():
return _("Please add some cards first.<p/>")
d = self.deck
html="<h1>" + _("Deck Statistics") + "</h1>"
html += _("Deck created: <b>%s</b> ago<br>") % self.createdTimeStr()
total = d.cardCount()
new = d.newCardCount()
young = d.youngCardCount()
old = d.matureCardCount()
newP = new / float(total) * 100
youngP = young / float(total) * 100
oldP = old / float(total) * 100
stats = d.getStats()
(stats["new"], stats["newP"]) = (new, newP)
(stats["old"], stats["oldP"]) = (old, oldP)
(stats["young"], stats["youngP"]) = (young, youngP)
html += _("Total number of cards: <b>%d</b><br><br>") % total
html += _("<b>Card counts</b><br>")
html += _("Mature cards: <b>%(old)d</b> "
"(<b>%(oldP)0.2f%%</b>)<br>") % stats
html += _("Young cards: <b>%(young)d</b> "
"(<b>%(youngP)0.2f%%</b>)<br>") % stats
html += _("Unseen cards: <b>%(new)d</b> "
"(<b>%(newP)0.2f%%</b>)<br><br>") % stats
html += _("<b>Correct answers</b><br>")
html += _("Mature cards: <b>%(gMatureYes%)0.1f%%</b> "
"(<b>%(gMatureYes)d</b> of <b>%(gMatureTotal)d</b>)<br>") % stats
html += _("Young cards: <b>%(gYoungYes%)0.1f%%</b> "
"(<b>%(gYoungYes)d</b> of <b>%(gYoungTotal)d</b>)<br>") % stats
html += _("First-seen cards: <b>%(gNewYes%)0.1f%%</b> "
"(<b>%(gNewYes)d</b> of <b>%(gNewTotal)d</b>)<br><br>") % stats
# average pending time
existing = d.cardCount() - d.newCardCount()
avgInt = self.getAverageInterval()
def tr(a, b):
return "<tr><td>%s</td><td align=right>%s</td></tr>" % (a, b)
if existing and avgInt:
html += _("<b>Averages</b><br>")
html += "<table width=200>"
html += tr(_("Interval"), _("<b>%0.0f</b> days") % avgInt)
html += tr(_("Average reps"), _("<b>%0.1f</b> cards/day") % (
self.getSumInverseRoundInterval()))
html += tr(_("Reps next week"), _("<b>%0.1f</b> cards/day") % (
self.getWorkloadPeriod(7)))
html += tr(_("Reps next month"), _("<b>%0.1f</b> cards/day") % (
self.getWorkloadPeriod(30)))
html += tr(_("Reps last week"), _("<b>%0.1f</b> cards/day") % (
self.getPastWorkloadPeriod(7)))
html += tr(_("Reps last month"), _("<b>%0.1f</b> cards/day") % (
self.getPastWorkloadPeriod(30)))
html += tr(_("Avg. added"), _("<b>%(a)d</b>/day, <b>%(b)d</b>/mon") % {
'a': self.newAverage(), 'b': self.newAverage()*30})
np = self.getNewPeriod(7)
html += tr(_("Added last week"), _("<b>%(a)d</b> (<b>%(b)0.1f</b>/day)") % (
{'a': np, 'b': np / float(7)}))
np = self.getNewPeriod(30)
html += tr(_("Added last month"), _("<b>%(a)d</b> (<b>%(b)0.1f</b>/day)") % (
{'a': np, 'b': np / float(30)}))
np = self.getFirstPeriod(7)
html += tr(_("First last week"), _("<b>%(a)d</b> (<b>%(b)0.1f</b>/day)") % (
{'a': np, 'b': np / float(7)}))
np = self.getFirstPeriod(30)
html += tr(_("First last month"), _("<b>%(a)d</b> (<b>%(b)0.1f</b>/day)") % (
{'a': np, 'b': np / float(30)}))
html += "</table>"
return html
def getAverageInterval(self):
return self.deck.s.scalar(
"select sum(interval) / count(interval) from cards "
"where cards.reps > 0") or 0
def intervalReport(self, intervals, labels, total):
boxes = self.splitIntoIntervals(intervals)
keys = boxes.keys()
keys.sort()
html = ""
for key in keys:
html += ("<tr><td align=right>%s</td><td align=right>" +
"%d</td><td align=right>%0.2f%%</td></tr>") % (
labels[key],
boxes[key],
boxes[key] / float(total) * 100)
return html
def splitIntoIntervals(self, intervals):
boxes = {}
n = 0
for i in range(len(intervals) - 1):
(min, max) = (intervals[i], intervals[i+1])
for c in self.deck:
if c.interval > min and c.interval <= max:
boxes[n] = boxes.get(n, 0) + 1
n += 1
return boxes
def newAverage(self):
"Average number of new cards added each day."
return self.deck.cardCount() / max(1, self.ageInDays())
def createdTimeStr(self):
return anki.utils.fmtTimeSpan(time.time() - self.deck.created)
def ageInDays(self):
return (time.time() - self.deck.created) / 86400.0
def getSumInverseRoundInterval(self):
return self.deck.s.scalar(
"select sum(1/round(max(interval, 1)+0.5)) from cards "
"where cards.reps > 0 "
"and priority > 0") or 0
def getWorkloadPeriod(self, period):
cutoff = time.time() + 86400 * period
return (self.deck.s.scalar("""
select count(id) from cards
where combinedDue < :cutoff
and priority > 0""", cutoff=cutoff) or 0) / float(period)
def getPastWorkloadPeriod(self, period):
cutoff = time.time() - 86400 * period
return (self.deck.s.scalar("""
select count(id) from reviewHistory
where time > :cutoff""", cutoff=cutoff) or 0) / float(period)
def getNewPeriod(self, period):
cutoff = time.time() - 86400 * period
return (self.deck.s.scalar("""
select count(id) from cards
where created > :cutoff""", cutoff=cutoff) or 0)
def getFirstPeriod(self, period):
cutoff = time.time() - 86400 * period
return (self.deck.s.scalar("""
select count(id) from reviewHistory
where reps = 1 and time > :cutoff""", cutoff=cutoff) or 0)
# Kanji stats
##########################################################################
def asHTMLDocument(text):
"Return text wrapped in a HTML document."
return ("""
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<meta http-equiv=content-type content="text/html; charset=utf-8">
</head>
<body>
%s
</body>
</html>
""" % text.encode("utf-8"))
def isKanji(unichar):
try:
return unicodedata.name(unichar).find('CJK UNIFIED IDEOGRAPH') >= 0
except ValueError:
# a control character
return False
class KanjiStats(object):
def __init__(self, deck):
self.deck = deck
self._gradeHash = dict()
for (name, chars), grade in zip(self.kanjiGrades,
xrange(len(self.kanjiGrades))):
for c in chars:
self._gradeHash[c] = grade
def kanjiGrade(self, unichar):
return self._gradeHash.get(unichar, 0)
# FIXME: as it's html, the width doesn't matter
def kanjiCountStr(self, gradename, count, total=0, width=0):
d = {'count': self.rjustfig(count, width), 'gradename': gradename}
if total:
d['total'] = self.rjustfig(total, width)
d['percent'] = float(count)/total*100
return _("%(gradename)s: %(count)s of %(total)s (%(percent)0.1f%%).") % d
else:
return _("%(count)s %(gradename)s kanji.") % d
def rjustfig(self, n, width):
n = unicode(n)
return n + "&nbsp;" * (width - len(n))
def genKanjiSets(self):
self.kanjiSets = [set([]) for g in self.kanjiGrades]
all = "".join(self.deck.s.column0("""
select value from cards, fields
where
cards.reps > 0 and
cards.factId = fields.factId
"""))
for u in all:
if isKanji(u):
self.kanjiSets[self.kanjiGrade(u)].add(u)
def report(self):
self.genKanjiSets()
counts = [(name, len(found), len(all)) \
for (name, all), found in zip(self.kanjiGrades, self.kanjiSets)]
out = (_("<h1>Kanji statistics</h1>The %d seen cards in this deck "
"contain:") % self.deck.seenCardCount() +
"<br/><ul>" +
# total kanji
_("<li>%d total unique kanji.</li>") %
sum([c[1] for c in counts]) +
# total joyo
"<li>%s</li>" % self.kanjiCountStr(
u'Jouyou',sum([c[1] for c in counts[1:8]]),
sum([c[2] for c in counts[1:8]])) +
# total jinmei
"<li>%s</li>" % self.kanjiCountStr(*counts[8]) +
# total non-joyo
"<li>%s</li>" % self.kanjiCountStr(*counts[0]))
out += "</ul><p/>" + _(u"Jouyou levels:") + "<p/><ul>"
L = ["<li>" + self.kanjiCountStr(c[0],c[1],c[2], width=3) + "</li>"
for c in counts[1:8]]
out += "".join(L)
out += "</ul>"
return out
def missingReport(self):
out = "<h1>Missing kanji</h1>"
for grade in range(1, 9):
missing = "".join(self.missingInGrade(grade))
if not missing:
continue
out += "<h2>" + self.kanjiGrades[grade][0] + "</h2>"
out += "<font size=+4>"
while 1:
if not missing:
break
# edict will take up to about 10 kanji at once
out += self.edictKanjiLink(missing[0:10])
missing = missing[10:]
out += "</font>"
return out + "<br/>"
def edictKanjiLink(self, kanji):
base="http://www.csse.monash.edu.au/~jwb/cgi-bin/wwwjdic.cgi?1MMJ"
url=base + kanji
return '<a href="%s">%s</a>' % (url, kanji)
def missingInGrade(self, gradeNum):
existingKanji = self.kanjiSets[gradeNum]
totalKanji = self.kanjiGrades[gradeNum][1]
return [k for k in totalKanji if k not in existingKanji]
kanjiGrades = [
(u'non-jouyou', ''),
(u'Grade 1', u'一右雨円王音下火花貝学気休玉金九空月犬見五口校左三山四子糸字耳七車手十出女小上森人水正生青石赤先千川早草足村大男竹中虫町天田土二日入年白八百文本名木目夕立力林六'),
(u'Grade 2', u'引羽雲園遠黄何夏家科歌画会回海絵外角楽活間丸岩顔帰汽記弓牛魚京強教近兄形計元原言古戸午後語交光公工広考行高合国黒今才細作算姉市思止紙寺時自室社弱首秋週春書少場色食心新親図数星晴声西切雪線船前組走多太体台谷知地池茶昼朝長鳥直通弟店点電冬刀東当答頭同道読内南肉馬買売麦半番父風分聞米歩母方北妹毎万明鳴毛門夜野矢友曜用来理里話'),
(u'Grade 3', u'悪安暗委意医育員飲院運泳駅央横屋温化荷界開階寒感漢館岸期起客宮急球究級去橋業局曲銀区苦具君係軽決血研県庫湖向幸港号根祭坂皿仕使始指死詩歯事持次式実写者主取守酒受州拾終習集住重宿所暑助勝商昭消章乗植深申真神身進世整昔全想相送息速族他打対待代第題炭短談着柱注丁帳調追定庭笛鉄転登都度島投湯等豆動童農波配倍箱畑発反板悲皮美鼻筆氷表病秒品負部服福物平返勉放味命面問役薬油有由遊予様洋羊葉陽落流旅両緑礼列練路和'),
(u'Grade 4', u'愛案以位囲胃衣印栄英塩億加果課貨芽改械害街各覚完官管観関願喜器希旗機季紀議救求泣給挙漁競共協鏡極訓軍郡型径景芸欠結健建験固候功好康航告差最菜材昨刷察札殺参散産残司史士氏試児治辞失借種周祝順初唱松焼照省笑象賞信臣成清静席積折節説戦浅選然倉巣争側束続卒孫帯隊達単置仲貯兆腸低停底的典伝徒努灯働堂得特毒熱念敗梅博飯費飛必標票不付夫府副粉兵別変辺便包法望牧末満未脈民無約勇要養浴利陸料良量輪類令例冷歴連労老録'),
(u'Grade 5', u'圧易移因営永衛液益演往応恩仮価可河過賀解快格確額刊幹慣眼基寄規技義逆久旧居許境興均禁句群経潔件券検険減現限個故護効厚構耕講鉱混査再妻採災際在罪財桜雑賛酸師志支枝資飼似示識質舎謝授修術述準序承招証常情条状織職制勢性政精製税績責接設絶舌銭祖素総像増造則測属損態貸退団断築張提程敵適統導銅徳独任燃能破判版犯比肥非備俵評貧婦富布武復複仏編弁保墓報豊暴貿防務夢迷綿輸余預容率略留領'),
(u'Grade 6', u'異遺域宇映延沿我灰拡閣革割株巻干看簡危揮机貴疑吸供胸郷勤筋敬系警劇激穴憲権絹厳源呼己誤后孝皇紅鋼降刻穀骨困砂座済裁策冊蚕姿私至視詞誌磁射捨尺若樹収宗就衆従縦縮熟純処署諸除傷将障城蒸針仁垂推寸盛聖誠宣専泉洗染善創奏層操窓装臓蔵存尊宅担探誕暖段値宙忠著庁潮頂賃痛展党糖討届難乳認納脳派俳拝背肺班晩否批秘腹奮並閉陛片補暮宝訪亡忘棒枚幕密盟模訳優郵幼欲翌乱卵覧裏律臨朗論'),
(u'JuniorHS', u'亜哀握扱依偉威尉慰為維緯違井壱逸稲芋姻陰隠韻渦浦影詠鋭疫悦謁越閲宴援炎煙猿縁鉛汚凹奥押欧殴翁沖憶乙卸穏佳嫁寡暇架禍稼箇華菓蚊雅餓介塊壊怪悔懐戒拐皆劾慨概涯該垣嚇核殻獲穫較郭隔岳掛潟喝括渇滑褐轄且刈乾冠勘勧喚堪寛患憾換敢棺款歓汗環甘監緩缶肝艦貫還鑑閑陥含頑企奇岐幾忌既棋棄祈軌輝飢騎鬼偽儀宜戯擬欺犠菊吉喫詰却脚虐丘及朽窮糾巨拒拠虚距享凶叫峡恐恭挟況狂狭矯脅響驚仰凝暁斤琴緊菌襟謹吟駆愚虞偶遇隅屈掘靴繰桑勲薫傾刑啓契恵慶憩掲携渓継茎蛍鶏迎鯨撃傑倹兼剣圏堅嫌懸献肩謙賢軒遣顕幻弦玄孤弧枯誇雇顧鼓互呉娯御悟碁侯坑孔巧恒慌抗拘控攻更江洪溝甲硬稿絞綱肯荒衡貢購郊酵項香剛拷豪克酷獄腰込墾婚恨懇昆紺魂佐唆詐鎖債催宰彩栽歳砕斎載剤咲崎削搾索錯撮擦傘惨桟暫伺刺嗣施旨祉紫肢脂諮賜雌侍慈滋璽軸執湿漆疾芝赦斜煮遮蛇邪勺爵酌釈寂朱殊狩珠趣儒寿需囚愁秀臭舟襲酬醜充柔汁渋獣銃叔淑粛塾俊瞬准循旬殉潤盾巡遵庶緒叙徐償匠升召奨宵尚床彰抄掌昇晶沼渉焦症硝礁祥称粧紹肖衝訟詔詳鐘丈冗剰壌嬢浄畳譲醸錠嘱飾殖触辱伸侵唇娠寝審慎振浸紳薪診辛震刃尋甚尽迅陣酢吹帥炊睡粋衰遂酔錘随髄崇枢据杉澄瀬畝是姓征牲誓請逝斉隻惜斥析籍跡拙摂窃仙占扇栓潜旋繊薦践遷銑鮮漸禅繕塑措疎礎租粗訴阻僧双喪壮捜掃挿曹槽燥荘葬藻遭霜騒憎贈促即俗賊堕妥惰駄耐怠替泰滞胎袋逮滝卓択拓沢濯託濁諾但奪脱棚丹嘆淡端胆鍛壇弾恥痴稚致遅畜蓄逐秩窒嫡抽衷鋳駐弔彫徴懲挑眺聴脹超跳勅朕沈珍鎮陳津墜塚漬坪釣亭偵貞呈堤帝廷抵締艇訂逓邸泥摘滴哲徹撤迭添殿吐塗斗渡途奴怒倒凍唐塔悼搭桃棟盗痘筒到謄踏逃透陶騰闘洞胴峠匿督篤凸突屯豚曇鈍縄軟尼弐如尿妊忍寧猫粘悩濃把覇婆廃排杯輩培媒賠陪伯拍泊舶薄迫漠爆縛肌鉢髪伐罰抜閥伴帆搬畔繁般藩販範煩頒盤蛮卑妃彼扉披泌疲碑罷被避尾微匹姫漂描苗浜賓頻敏瓶怖扶敷普浮符腐膚譜賦赴附侮舞封伏幅覆払沸噴墳憤紛雰丙併塀幣弊柄壁癖偏遍舗捕穂募慕簿倣俸奉峰崩抱泡砲縫胞芳褒邦飽乏傍剖坊妨帽忙房某冒紡肪膨謀僕墨撲朴没堀奔翻凡盆摩磨魔麻埋膜又抹繭慢漫魅岬妙眠矛霧婿娘銘滅免茂妄猛盲網耗黙戻紋匁厄躍柳愉癒諭唯幽悠憂猶裕誘雄融与誉庸揚揺擁溶窯謡踊抑翼羅裸頼雷絡酪欄濫吏履痢離硫粒隆竜慮虜了僚寮涼猟療糧陵倫厘隣塁涙累励鈴隷零霊麗齢暦劣烈裂廉恋錬炉露廊楼浪漏郎賄惑枠湾腕'),
(u'Jinmeiyou', u'阿葵茜渥旭梓絢綾鮎杏伊惟亥郁磯允胤卯丑唄叡瑛艶苑於旺伽嘉茄霞魁凱馨叶樺鎌茅侃莞巌伎嬉毅稀亀誼鞠橘亨匡喬尭桐錦欣欽芹衿玖矩駒熊栗袈圭慧桂拳絃胡虎伍吾梧瑚鯉倖宏弘昂晃浩紘鴻嵯沙瑳裟哉采冴朔笹皐燦獅爾蒔汐鹿偲紗洲峻竣舜駿淳醇曙渚恕庄捷昌梢菖蕉丞穣晋榛秦須翠瑞嵩雛碩曽爽惣綜聡蒼汰黛鯛鷹啄琢只辰巽旦檀智猪暢蝶椎槻蔦椿紬鶴悌汀禎杜藤憧瞳寅酉惇敦奈那凪捺楠虹乃之巴萩肇鳩隼斐緋眉柊彦媛彪彬芙楓蕗碧甫輔朋萌鳳鵬睦槙柾亦麿巳稔椋孟也冶耶弥靖佑宥柚湧祐邑楊耀蓉遥嵐藍蘭李梨璃琉亮凌瞭稜諒遼琳麟瑠伶嶺怜玲蓮呂禄倭亘侑勁奎崚彗昴晏晨晟暉栞椰毬洸洵滉漱澪燎燿瑶皓眸笙綺綸翔脩茉莉菫詢諄赳迪頌颯黎凜熙')
]

210
anki/stdmodels.py Normal file
View file

@ -0,0 +1,210 @@
# -*- coding: utf-8 -*-
# Copyright: Damien Elmes <anki@ichi2.net>
# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html
"""\
Standard models
==============================================================
"""
from anki.models import Model, CardModel, FieldModel
from anki.lang import _
models = {}
def byName(name):
fn = models.get(name)
if fn:
return fn()
raise ValueError("No such model available!")
def names():
return models.keys()
# these are provided for convenience. all of the fields can be changed in real
# time and they will be stored with the deck.
# Basic
##########################################################################
def BasicModel():
m = Model(_('Basic'),
_('A basic flashcard with a front and a back.\n'
'Questions are asked from front to back by default.\n\n'
'Please consider customizing this model, rather than\n'
'using it verbatim: field names like "expression" are\n'
'clearer than "front" and "back", and will ensure\n'
'that your entries are consistent.'))
m.addFieldModel(FieldModel(u'Front', _('A question.'), True, True))
m.addFieldModel(FieldModel(u'Back', _('The answer.'), True, True))
m.addCardModel(CardModel(u'Front to back', _('Front to back'),
u'%(Front)s', u'%(Back)s'))
m.addCardModel(CardModel(u'Back to front', _('Back to front'),
u'%(Back)s', u'%(Front)s', active=False))
return m
models['Basic'] = BasicModel
# Japanese
##########################################################################
def JapaneseModel():
m = Model(_("Japanese"),
_("""
The reading field is automatically generated by default,
and shows the reading for the expression. For words that
are normally written in hiragana or katakana and don't
need a reading, you can put the word in the expression
field, and leave the reading field blank. A reading will
will not automatically be generated for words written
in only hiragana or katakana.
Note that the automatic generation of meaning is not
perfect, and should be checked before adding cards.""".strip()))
# expression
f = FieldModel(u'Expression',
_('A word or expression written in Kanji.'), True, True)
font = u"Mincho"
f.quizFontSize = 72
f.quizFontFamily = font
f.editFontFamily = font
f.features = u"Reading source"
m.addFieldModel(f)
# meaning
m.addFieldModel(FieldModel(
u'Meaning',
_('A description in your native language, or Japanese'), True, True))
# reading
f = FieldModel(u'Reading',
_('Automatically generated by default.'), False, False)
f.quizFontFamily = font
f.editFontFamily = font
f.features = u"Reading destination"
m.addFieldModel(f)
m.addCardModel(CardModel(u"Production", _(
"Actively test your recall by producing the target expression"),
u"%(Meaning)s",
u"%(Expression)s<br>%(Reading)s"))
m.addCardModel(CardModel(u"Recognition", _(
"Test your ability to recognize the target expression"),
u"%(Expression)s",
u"%(Reading)s<br>%(Meaning)s"))
m.features = u"Japanese"
m.tags = u"Japanese"
return m
models['Japanese'] = JapaneseModel
# English
##########################################################################
def EnglishModel():
m = Model(_("English"),
_("""
Enter the English expression you want to learn in the 'Expression' field.
Enter a description in Japanese or English in the 'Meaning' field.""".strip()))
m.addFieldModel(FieldModel(u'Expression'))
m.addFieldModel(FieldModel(u'Meaning'))
m.addCardModel(CardModel(
u"Production", _("From the meaning to the English expression."),
u"%(Meaning)s", u"%(Expression)s"))
m.addCardModel(CardModel(
u"Recognition", _("From the English expression to the meaning."),
u"%(Expression)s", u"%(Meaning)s", active=False))
m.tags = u"English"
return m
models['English'] = EnglishModel
# Heisig
##########################################################################
def HeisigModel():
m = Model(_("Heisig"),
_("""
A format suitable for Heisig's "Remembering the Kanji".
You are tested from the keyword to the kanji.
Layout of the test is based on the great work at
http://kanji.koohii.com/
The link in the question will list user-contributed
stories. A free login is required.""".strip()))
font = u"Mincho"
f = FieldModel(u'Kanji')
f.quizFontSize = 150
f.quizFontFamily = font
f.editFontFamily = font
m.addFieldModel(f)
m.addFieldModel(FieldModel(u'Keyword'))
m.addFieldModel(FieldModel(u'Story', u"", False, False))
m.addFieldModel(FieldModel(u'Stroke count', u"", False, False))
m.addFieldModel(FieldModel(u'Heisig number', required=False))
m.addFieldModel(FieldModel(u'Lesson number', u"", False, False))
m.addCardModel(CardModel(
u"Production", _("From the keyword to the Kanji."),
u"<a href=\"http://kanji.koohii.com/study?framenum="
u"%(text:Heisig number)s\">%(Keyword)s</a><br>",
u"%(Kanji)s<br><table width=150><tr><td align=left>"
u"画数%(Stroke count)s</td><td align=right>"
u"%(Heisig number)s</td></tr></table>"))
m.tags = u"Heisig"
return m
models['Heisig'] = HeisigModel
# Chinese: Mandarin & Cantonese
##########################################################################
def CantoneseModel():
m = Model(_("Cantonese"),
u"")
f = FieldModel(u'Expression',
_('A word or expression written in Hanzi.'))
f.quizFontSize = 72
f.features = u"Reading source"
m.addFieldModel(f)
m.addFieldModel(FieldModel(
u'Meaning', _('A description in your native language, or Cantonese')))
f = FieldModel(u'Reading',
_('Automatically generated by default.'), False, False)
f.features = u"Reading destination"
m.addFieldModel(f)
m.addCardModel(CardModel(u"Production", _(
"Actively test your recall by producing the target expression"),
u"%(Meaning)s",
u"%(Expression)s<br>%(Reading)s"))
m.addCardModel(CardModel(u"Recognition", _(
"Test your ability to recognize the target expression"),
u"%(Expression)s",
u"%(Reading)s<br>%(Meaning)s"))
m.features = u"Cantonese"
m.tags = u"Cantonese"
return m
models['Cantonese'] = CantoneseModel
def MandarinModel():
m = Model(_("Mandarin"),
u"")
f = FieldModel(u'Expression',
_('A word or expression written in Hanzi.'))
f.quizFontSize = 72
f.features = u"Reading source"
m.addFieldModel(f)
m.addFieldModel(FieldModel(
u'Meaning', _(
'A description in your native language, or Mandarin')))
f = FieldModel(u'Reading',
_('Automatically generated by default.'),
False, False)
f.features = u"Reading destination"
m.addFieldModel(f)
m.addCardModel(CardModel(u"Production", _(
"Actively test your recall by producing the target expression"),
u"%(Meaning)s",
u"%(Expression)s<br>%(Reading)s"))
m.addCardModel(CardModel(u"Recognition", _(
"Test your ability to recognize the target expression"),
u"%(Expression)s",
u"%(Reading)s<br>%(Meaning)s"))
m.features = u"Mandarin"
m.tags = u"Mandarin"
return m
models['Mandarin'] = MandarinModel

821
anki/sync.py Normal file
View file

@ -0,0 +1,821 @@
# -*- coding: utf-8 -*-
# Copyright: Damien Elmes <anki@ichi2.net>
# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html
"""\
Synchronisation
==============================
Support for keeping two decks synchronized. Both local syncing and syncing
over HTTP are supported.
Server implements the following calls:
getDecks(): return a list of deck names & modtimes
summary(lastSync): a list of all objects changed after lastSync
applyPayload(payload): apply any sent changes and return any changed remote
objects
createDeck(name): create a deck on the server
"""
__docformat__ = 'restructuredtext'
import zlib, re, urllib, urllib2, socket, simplejson, time
import os, base64
from datetime import date
import anki, anki.deck, anki.cards
from anki.errors import *
from anki.models import Model, FieldModel, CardModel
from anki.facts import Fact, Field
from anki.cards import Card
from anki.stats import Stats, globalStats
from anki.history import CardHistoryEntry
from anki.stats import dailyStats, globalStats
from anki.media import checksum
from anki.utils import ids2str
from anki.lang import _
if simplejson.__version__ < "1.7.3":
raise "SimpleJSON must be 1.7.3 or later."
# Protocol 3 code
##########################################################################
class SyncTools(object):
def __init__(self, deck=None):
self.deck = deck
self.diffs = {}
self.serverExcludedTags = []
# Control
##########################################################################
def setServer(self, server):
self.server = server
def sync(self):
"Sync two decks locally. Reimplement this for finer control."
if not self.prepareSync():
return
sums = self.summaries()
payload = self.genPayload(sums)
res = self.server.applyPayload(payload)
self.applyPayloadReply(res)
def prepareSync(self):
"Sync setup. True if sync needed."
self.localTime = self.modified()
self.remoteTime = self.server.modified()
if self.localTime == self.remoteTime:
return False
l = self._lastSync(); r = self.server._lastSync()
if l != r:
self.lastSync = min(l, r) - 600
else:
self.lastSync = l
return True
def summaries(self):
return (self.summary(self.lastSync),
self.server.summary(self.lastSync))
def genPayload(self, summaries):
(lsum, rsum) = summaries
self.preSyncRefresh()
payload = {}
# first, handle models, facts and cards
for key in self.keys():
diff = self.diffSummary(lsum, rsum, key)
payload["added-" + key] = self.getObjsFromKey(diff[0], key)
payload["deleted-" + key] = diff[1]
payload["missing-" + key] = diff[2]
self.deleteObjsFromKey(diff[3], key)
# handle the remainder
if self.localTime > self.remoteTime:
payload['deck'] = self.bundleDeck()
payload['stats'] = self.bundleStats()
payload['history'] = self.bundleHistory()
self.deck.lastSync = self.deck.modified
return payload
def applyPayload(self, payload):
reply = {}
self.preSyncRefresh()
# model, facts and cards
for key in self.keys():
# send back any requested
reply['added-' + key] = self.getObjsFromKey(
payload['missing-' + key], key)
self.updateObjsFromKey(payload['added-' + key], key)
self.deleteObjsFromKey(payload['deleted-' + key], key)
# send back deck-related stuff if it wasn't sent to us
if not 'deck' in payload:
reply['deck'] = self.bundleDeck()
reply['stats'] = self.bundleStats()
reply['history'] = self.bundleHistory()
self.deck.lastSync = self.deck.modified
else:
self.updateDeck(payload['deck'])
self.updateStats(payload['stats'])
self.updateHistory(payload['history'])
self.postSyncRefresh()
# rebuild priorities on server
cardIds = [x[0] for x in reply['added-cards']]
self.rebuildPriorities(cardIds, self.serverExcludedTags)
return reply
def applyPayloadReply(self, reply):
# model, facts and cards
for key in self.keys():
self.updateObjsFromKey(reply['added-' + key], key)
# deck
if 'deck' in reply:
self.updateDeck(reply['deck'])
self.updateStats(reply['stats'])
self.updateHistory(reply['history'])
self.postSyncRefresh()
# rebuild priorities on client
cardIds = [x[0] for x in reply['added-cards']]
self.rebuildPriorities(cardIds)
def rebuildPriorities(self, cardIds, extraExcludes=[]):
where = "and cards.id in %s" % ids2str(cardIds)
self.deck.updateAllPriorities(extraExcludes=extraExcludes,
where=where)
def postSyncRefresh(self):
"Flush changes to DB, and reload object associations."
self.deck.s.flush()
self.deck.s.refresh(self.deck)
self.deck.currentModel
def preSyncRefresh(self):
# ensure global stats are available (queue may not be built)
self.deck._globalStats = globalStats(self.deck.s)
def payloadChanges(self, payload):
h = {
'lf': len(payload['added-facts']['facts']),
'rf': len(payload['missing-facts']),
'lc': len(payload['added-cards']),
'rc': len(payload['missing-cards']),
'lm': len(payload['added-models']),
'rm': len(payload['missing-models']),
}
if self.server.mediaSupported:
h['lM'] = len(payload['added-media'])
h['rM'] = len(payload['missing-media'])
return h
def payloadChangeReport(self, payload):
p = self.payloadChanges(payload)
if self.server.mediaSupported:
p['media'] = (
"<tr><td>Media</td><td>%(lM)d</td><td>%(rM)d</td></tr>" % p)
else:
p['media'] = (
"<tr><td>Media</td><td>off</td><td>off</td></tr>" % p)
return _("""\
<table width=500>
<tr><td><b>Added/Changed</b></td><td><b>Here</b></td><td><b>Server</b></td></tr>
<tr><td>Cards</td><td>%(lc)d</td><td>%(rc)d</td></tr>
<tr><td>Facts</td><td>%(lf)d</td><td>%(rf)d</td></tr>
<tr><td>Models</td><td>%(lm)d</td><td>%(rm)d</td></tr>
%(media)s
</table>""") % p
# Summaries
##########################################################################
def summary(self, lastSync):
"Generate a full summary of modtimes for two-way syncing."
# ensure we're flushed first
self.deck.s.flush()
return {
# cards
"cards": self.realTuples(self.deck.s.all(
"select id, modified from cards where modified > :mod",
mod=lastSync)),
"delcards": self.realTuples(self.deck.s.all(
"select cardId, deletedTime from cardsDeleted "
"where deletedTime > :mod", mod=lastSync)),
# facts
"facts": self.realTuples(self.deck.s.all(
"select id, modified from facts where modified > :mod",
mod=lastSync)),
"delfacts": self.realTuples(self.deck.s.all(
"select factId, deletedTime from factsDeleted "
"where deletedTime > :mod", mod=lastSync)),
# models
"models": self.realTuples(self.deck.s.all(
"select id, modified from models where modified > :mod",
mod=lastSync)),
"delmodels": self.realTuples(self.deck.s.all(
"select modelId, deletedTime from modelsDeleted "
"where deletedTime > :mod", mod=lastSync)),
# media
"media": self.realTuples(self.deck.s.all(
"select id, created from media where created > :mod",
mod=lastSync)),
"delmedia": self.realTuples(self.deck.s.all(
"select mediaId, deletedTime from mediaDeleted "
"where deletedTime > :mod", mod=lastSync)),
}
# Diffing
##########################################################################
def diffSummary(self, localSummary, remoteSummary, key):
# list of ids on both ends
lexists = localSummary[key]
ldeleted = localSummary["del"+key]
rexists = remoteSummary[key]
rdeleted = remoteSummary["del"+key]
ldeletedIds = dict(ldeleted)
rdeletedIds = dict(rdeleted)
# to store the results
locallyEdited = []
locallyDeleted = []
remotelyEdited = []
remotelyDeleted = []
# build a hash of all ids, with value (localMod, remoteMod).
# deleted/nonexisting cards are marked with a modtime of None.
ids = {}
for (id, mod) in rexists:
ids[id] = [None, mod]
for (id, mod) in rdeleted:
ids[id] = [None, None]
for (id, mod) in lexists:
if id in ids:
ids[id][0] = mod
else:
ids[id] = [mod, None]
for (id, mod) in ldeleted:
if id in ids:
ids[id][0] = None
else:
ids[id] = [None, None]
# loop through the hash, determining differences
for (id, (localMod, remoteMod)) in ids.items():
if localMod and remoteMod:
# changed/existing on both sides
if localMod < remoteMod:
remotelyEdited.append(id)
elif localMod > remoteMod:
locallyEdited.append(id)
elif localMod and not remoteMod:
# if it's missing on server or newer here, sync
if (id not in rdeletedIds or
rdeletedIds[id] < localMod):
locallyEdited.append(id)
else:
remotelyDeleted.append(id)
elif remoteMod and not localMod:
# if it's missing locally or newer there, sync
if (id not in ldeletedIds or
ldeletedIds[id] < remoteMod):
remotelyEdited.append(id)
else:
locallyDeleted.append(id)
else:
if id in ldeletedIds and id not in rdeletedIds:
locallyDeleted.append(id)
elif id in rdeletedIds and id not in ldeletedIds:
remotelyDeleted.append(id)
return (locallyEdited, locallyDeleted,
remotelyEdited, remotelyDeleted)
# Models
##########################################################################
def getModels(self, ids):
return [self.bundleModel(id) for id in ids]
def bundleModel(self, id):
"Return a model representation suitable for transport."
# force load of lazy attributes
mod = self.deck.s.query(Model).get(id)
mod.fieldModels; mod.cardModels
m = self.dictFromObj(mod)
m['fieldModels'] = [self.bundleFieldModel(fm) for fm in m['fieldModels']]
m['cardModels'] = [self.bundleCardModel(fm) for fm in m['cardModels']]
return m
def bundleFieldModel(self, fm):
d = self.dictFromObj(fm)
if 'model' in d: del d['model']
return d
def bundleCardModel(self, cm):
d = self.dictFromObj(cm)
if 'model' in d: del d['model']
return d
def updateModels(self, models):
for model in models:
local = self.getModel(model['id'])
# avoid overwriting any existing card/field models
fms = model['fieldModels']; del model['fieldModels']
cms = model['cardModels']; del model['cardModels']
self.applyDict(local, model)
self.mergeFieldModels(local, fms)
self.mergeCardModels(local, cms)
def getModel(self, id, create=True):
"Return a local model with same ID, or create."
for l in self.deck.models:
if l.id == id:
return l
if not create:
return
m = Model()
self.deck.models.append(m)
return m
def mergeFieldModels(self, model, fms):
ids = []
for fm in fms:
local = self.getFieldModel(model, fm)
self.applyDict(local, fm)
ids.append(fm['id'])
for fm in model.fieldModels:
if fm.id not in ids:
self.deck.deleteFieldModel(model, fm)
def getFieldModel(self, model, remote):
for fm in model.fieldModels:
if fm.id == remote['id']:
return fm
fm = FieldModel()
model.addFieldModel(fm)
return fm
def mergeCardModels(self, model, cms):
ids = []
for cm in cms:
local = self.getCardModel(model, cm)
self.applyDict(local, cm)
ids.append(cm['id'])
for cm in model.cardModels:
if cm.id not in ids:
self.deck.deleteCardModel(model, cm)
def getCardModel(self, model, remote):
for cm in model.cardModels:
if cm.id == remote['id']:
return cm
cm = CardModel()
model.addCardModel(cm)
return cm
def deleteModels(self, ids):
for id in ids:
model = self.getModel(id, create=False)
if model:
self.deck.deleteModel(model)
# Facts
##########################################################################
def getFacts(self, ids):
factIds = ids2str(ids)
return {
'facts': self.realTuples(self.deck.s.all("""
select id, modelId, created, modified, tags, spaceUntil, lastCardId from facts
where id in %s""" % factIds)),
'fields': self.realTuples(self.deck.s.all("""
select id, factId, fieldModelId, ordinal, value from fields
where factId in %s""" % factIds))
}
def updateFacts(self, factsdict):
facts = factsdict['facts']
fields = factsdict['fields']
if not facts:
return
# update facts first
dlist = [{
'id': f[0],
'modelId': f[1],
'created': f[2],
'modified': f[3],
'tags': f[4],
'spaceUntil': f[5],
'lastCardId': f[6]
} for f in facts]
self.deck.s.execute("""
insert or replace into facts
(id, modelId, created, modified, tags, spaceUntil, lastCardId)
values
(:id, :modelId, :created, :modified, :tags, :spaceUntil, :lastCardId)""", dlist)
# now fields
dlist = [{
'id': f[0],
'factId': f[1],
'fieldModelId': f[2],
'ordinal': f[3],
'value': f[4]
} for f in fields]
# delete local fields since ids may have changed
self.deck.s.execute(
"delete from fields where factId in %s" %
ids2str([f[0] for f in facts]))
# then update
self.deck.s.execute("""
insert or replace into fields
(id, factId, fieldModelId, ordinal, value)
values
(:id, :factId, :fieldModelId, :ordinal, :value)""", dlist)
def deleteFacts(self, ids):
self.deck.deleteFacts(ids)
# Cards
##########################################################################
def getCards(self, ids):
return self.realTuples(self.deck.s.all("""
select id, factId, cardModelId, created, modified, tags, ordinal,
priority, interval, lastInterval, due, lastDue, factor,
firstAnswered, reps, successive, averageTime, reviewTime, youngEase0,
youngEase1, youngEase2, youngEase3, youngEase4, matureEase0,
matureEase1, matureEase2, matureEase3, matureEase4, yesCount, noCount,
question, answer, lastFactor, spaceUntil, isDue, type, combinedDue
from cards where id in %s""" % ids2str(ids)))
def updateCards(self, cards):
if not cards:
return
dlist = [{'id': c[0],
'factId': c[1],
'cardModelId': c[2],
'created': c[3],
'modified': c[4],
'tags': c[5],
'ordinal': c[6],
'priority': c[7],
'interval': c[8],
'lastInterval': c[9],
'due': c[10],
'lastDue': c[11],
'factor': c[12],
'firstAnswered': c[13],
'reps': c[14],
'successive': c[15],
'averageTime': c[16],
'reviewTime': c[17],
'youngEase0': c[18],
'youngEase1': c[19],
'youngEase2': c[20],
'youngEase3': c[21],
'youngEase4': c[22],
'matureEase0': c[23],
'matureEase1': c[24],
'matureEase2': c[25],
'matureEase3': c[26],
'matureEase4': c[27],
'yesCount': c[28],
'noCount': c[29],
'question': c[30],
'answer': c[31],
'lastFactor': c[32],
'spaceUntil': c[33],
'isDue': c[34],
'type': c[35],
'combinedDue': c[36],
} for c in cards]
self.deck.s.execute("""
insert or replace into cards
(id, factId, cardModelId, created, modified, tags, ordinal,
priority, interval, lastInterval, due, lastDue, factor,
firstAnswered, reps, successive, averageTime, reviewTime, youngEase0,
youngEase1, youngEase2, youngEase3, youngEase4, matureEase0,
matureEase1, matureEase2, matureEase3, matureEase4, yesCount, noCount,
question, answer, lastFactor, spaceUntil, isDue, type, combinedDue,
relativeDelay)
values
(:id, :factId, :cardModelId, :created, :modified, :tags, :ordinal,
:priority, :interval, :lastInterval, :due, :lastDue, :factor,
:firstAnswered, :reps, :successive, :averageTime, :reviewTime, :youngEase0,
:youngEase1, :youngEase2, :youngEase3, :youngEase4, :matureEase0,
:matureEase1, :matureEase2, :matureEase3, :matureEase4, :yesCount,
:noCount, :question, :answer, :lastFactor, :spaceUntil, :isDue,
:type, :combinedDue, 0)""", dlist)
def deleteCards(self, ids):
self.deck.deleteCards(ids)
# Deck/stats/history
##########################################################################
def bundleDeck(self):
d = self.dictFromObj(self.deck)
del d['Session']
del d['engine']
del d['s']
del d['path']
del d['syncName']
del d['version']
# these may be deleted before bundling
if 'models' in d: del d['models']
if 'currentModel' in d: del d['currentModel']
return d
def updateDeck(self, deck):
self.applyDict(self.deck, deck)
self.deck.lastSync = self.deck.modified
def bundleStats(self):
def bundleStat(stat):
s = self.dictFromObj(stat)
s['day'] = s['day'].toordinal()
del s['id']
return s
lastDay = date.fromtimestamp(self.deck.lastSync)
ids = self.deck.s.column0(
"select id from stats where type = 1 and day >= :day", day=lastDay)
stat = Stats()
def statFromId(id):
stat.fromDB(self.deck.s, id)
return stat
stats = {
'global': bundleStat(self.deck._globalStats),
'daily': [bundleStat(statFromId(id)) for id in ids],
}
return stats
def updateStats(self, stats):
stats['global']['day'] = date.fromordinal(stats['global']['day'])
self.applyDict(self.deck._globalStats, stats['global'])
self.deck._globalStats.toDB(self.deck.s)
for record in stats['daily']:
record['day'] = date.fromordinal(record['day'])
stat = Stats()
id = self.deck.s.scalar("select id from stats where "
"type = :type and day = :day",
type=1, day=record['day'])
if id:
stat.fromDB(self.deck.s, id)
else:
stat.create(self.deck.s, 1, record['day'])
self.applyDict(stat, record)
stat.toDB(self.deck.s)
def bundleHistory(self):
def bundleHist(hist):
h = self.dictFromObj(hist)
del h['id']
return h
hst = self.deck.s.query(CardHistoryEntry).filter(
CardHistoryEntry.time > self.deck.lastSync)
return [bundleHist(h) for h in hst]
def updateHistory(self, history):
for h in history:
ent = CardHistoryEntry()
self.applyDict(ent, h)
self.deck.s.save(ent)
# Media
##########################################################################
def getMedia(self, ids):
return [(tuple(row),
base64.b64encode(self.getMediaData(row[1])))
for row in self.deck.s.all("""
select * from media where id in %s""" % ids2str(ids))]
def getMediaData(self, fname):
try:
return open(self.mediaPath(fname), "rb").read()
except (OSError, IOError):
return ""
def updateMedia(self, media):
meta = []
for (m, data) in media:
if not data:
continue
# ensure media is correctly checksummed and sized
fname = m[1]
size = m[2]
data = base64.b64decode(data)
assert len(data) == size
assert checksum(data) == os.path.splitext(fname)[0]
# write it out
self.addMediaFile(m, data)
# build meta
meta.append({
'id': m[0],
'filename': m[1],
'size': m[2],
'created': m[3],
'originalPath': m[4],
'description': m[5]})
# apply metadata
if meta:
self.deck.s.statements("""
insert or replace into media (id, filename, size, created,
originalPath, description)
values (:id, :filename, :size, :created, :originalPath,
:description)""", meta)
def deleteMedia(self, ids):
sids = ids2str(ids)
files = self.deck.s.column0(
"select filename from media where id in %s" % sids)
self.deck.s.statement("""
insert into mediaDeleted
select id, :now from media
where media.id in %s""" % sids, now=time.time())
self.deck.s.execute(
"delete from media where id in %s" % sids)
for file in files:
self.deleteMediaFile(file)
# the following routines are reimplemented by the anki server so that
# media can be shared and accounted
def addMediaFile(self, meta, decodedData):
fname = meta[1]
path = self.mediaPath(fname)
exists = os.path.exists(path)
if not exists:
open(path, "wb").write(decodedData)
def deleteMediaFile(self, file):
os.unlink(self.mediaPath(file))
def mediaPath(self, path):
"Return the path to store media in. Defaults to the deck media dir."
return os.path.join(self.deck.mediaDir(create=True), path)
# Tools
##########################################################################
def modified(self):
return self.deck.modified
def _lastSync(self):
return self.deck.lastSync
def unstuff(self, data):
"Uncompress and convert to unicode."
return simplejson.loads(zlib.decompress(data))
def stuff(self, data):
"Convert into UTF-8 and compress."
return zlib.compress(simplejson.dumps(data))
def dictFromObj(self, obj):
"Return a dict representing OBJ without any hidden db fields."
return dict([(k,v) for (k,v) in obj.__dict__.items()
if not k.startswith("_")])
def applyDict(self, obj, dict):
"Apply each element in DICT to OBJ in a way the ORM notices."
for (k,v) in dict.items():
setattr(obj, k, v)
def realTuples(self, result):
"Convert an SQLAlchemy response into a list of real tuples."
return [tuple(x) for x in result]
def getObjsFromKey(self, ids, key):
return getattr(self, "get" + key.capitalize())(ids)
def deleteObjsFromKey(self, ids, key):
return getattr(self, "delete" + key.capitalize())(ids)
def updateObjsFromKey(self, ids, key):
return getattr(self, "update" + key.capitalize())(ids)
# Local syncing
##########################################################################
standardKeys = ("models", "facts", "cards")
class SyncServer(SyncTools):
def __init__(self, deck=None):
SyncTools.__init__(self, deck)
self.mediaSupported = True
def keys(self):
if self.mediaSupported:
return standardKeys + ("media",)
return standardKeys
class SyncClient(SyncTools):
def keys(self):
if self.server.mediaSupported:
return standardKeys + ("media",)
return standardKeys
# HTTP proxy: act as a server and direct requests to the real server
##########################################################################
class HttpSyncServerProxy(SyncServer):
def __init__(self, user, passwd):
SyncServer.__init__(self)
self.decks = None
self.deckName = None
self.username = user
self.password = passwd
self.syncURL="http://anki.ichi2.net/sync/"
#self.syncURL="http://anki.ichi2.net:5001/sync/"
#self.syncURL="http://localhost/sync/"
self.protocolVersion = 2
def connect(self, clientVersion=""):
"Check auth, protocol & grab deck list."
if not self.decks:
d = self.runCmd("getDecks",
libanki=anki.version,
client=clientVersion)
if d['status'] != "OK":
raise SyncError(type="authFailed", status=d['status'])
self.mediaSupported = d['mediaSupported']
self.decks = d['decks']
self.timestamp = d['timestamp']
def hasDeck(self, deckName):
self.connect()
return deckName in self.decks.keys()
def availableDecks(self):
self.connect()
return self.decks.keys()
def createDeck(self, deckName):
ret = self.runCmd("createDeck", name=deckName.encode("utf-8"))
if not ret or ret['status'] != "OK":
raise SyncError(type="createFailed")
self.decks[deckName] = [0, 0]
def summary(self, lastSync):
return self.runCmd("summary",
lastSync=self.stuff(lastSync))
def modified(self):
self.connect()
return self.decks[self.deckName][0]
def _lastSync(self):
self.connect()
return self.decks[self.deckName][1]
def applyPayload(self, payload):
return self.runCmd("applyPayload",
payload=self.stuff(payload))
def runCmd(self, action, **args):
data = {"p": self.password,
"u": self.username}
if self.deckName:
data['d'] = self.deckName.encode("utf-8")
else:
data['d'] = None
data.update(args)
data = urllib.urlencode(data)
try:
f = urllib2.urlopen(self.syncURL + action, data)
except (urllib2.URLError, socket.error, socket.timeout):
raise SyncError(type="noResponse")
ret = f.read()
if not ret:
raise SyncError(type="noResponse")
return self.unstuff(ret)
# HTTP server: respond to proxy requests and return data
##########################################################################
class HttpSyncServer(SyncServer):
def __init__(self):
SyncServer.__init__(self)
self.protocolVersion = 2
self.decks = {}
self.deck = None
def summary(self, lastSync):
return self.stuff(SyncServer.summary(
self, self.unstuff(lastSync)))
def applyPayload(self, payload):
return self.stuff(SyncServer.applyPayload(self,
self.unstuff(payload)))
def getDecks(self, libanki, client):
return self.stuff({
"status": "OK",
"decks": self.decks,
"mediaSupported": self.mediaSupported,
"timestamp": time.time(),
})
def createDeck(self, name):
"Create a deck on the server. Not implemented."
return self.stuff("OK")

173
anki/utils.py Normal file
View file

@ -0,0 +1,173 @@
# -*- coding: utf-8 -*-
# Copyright: Damien Elmes <anki@ichi2.net>
# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html
"""\
Miscellaneous utilities
==============================
"""
__docformat__ = 'restructuredtext'
import re, os, random, time
from anki.db import *
from anki.lang import _, ngettext
timeTable = {
"years": lambda n: ngettext("%s year", "%s years", n),
"months": lambda n: ngettext("%s month", "%s months", n),
"days": lambda n: ngettext("%s day", "%s days", n),
"hours": lambda n: ngettext("%s hour", "%s hours", n),
"minutes": lambda n: ngettext("%s minute", "%s minutes", n),
"seconds": lambda n: ngettext("%s second", "%s seconds", n),
}
def fmtTimeSpan(time, pad=0, point=0):
"Return a string representing a time span (eg '2 days')."
(type, point) = optimalPeriod(time, point)
time = convertSecondsTo(time, type)
fmt = timeTable[type](_pluralCount(round(time, point)))
timestr = "%(a)d.%(b)df" % {'a': pad, 'b': point}
return ("%" + (fmt % timestr)) % time
def fmtTimeSpanPair(time1, time2):
(type, point) = optimalPeriod(time1, 0)
time1 = convertSecondsTo(time1, type)
time2 = convertSecondsTo(time2, type)
# a pair is always should always be read as plural
fmt = timeTable[type](2)
timestr = "%(a)d.%(b)df" % {'a': 0, 'b': point}
finalstr = "%s-%s" % (
('%' + timestr) % time1,
('%' + timestr) % time2)
return fmt % finalstr
def optimalPeriod(time, point):
if abs(time) < 60:
type = "seconds"
point -= 1
elif abs(time) < 3599:
type = "minutes"
elif abs(time) < 60 * 60 * 24:
type = "hours"
elif abs(time) < 60 * 60 * 24 * 30:
type = "days"
elif abs(time) < 60 * 60 * 24 * 365:
type = "months"
point += 1
else:
type = "years"
point += 1
return (type, max(point, 0))
def convertSecondsTo(seconds, type):
if type == "seconds":
return seconds
elif type == "minutes":
return seconds / 60.0
elif type == "hours":
return seconds / 3600.0
elif type == "days":
return seconds / 86400.0
elif type == "months":
return seconds / 2592000.0
elif type == "years":
return seconds / 31536000.0
assert False
def _pluralCount(time):
if round(time, 1) == 1:
return 1
return 2
def mergeTags(*args):
"Merge tag lists into a single string."
return ", ".join(set(parseTags(",".join(args))))
def parseTags(tags):
"Parse a string and return a list of tags."
tags = tags.split(",")
tags = [tag.strip() for tag in tags if tag.strip()]
return tags
def findTag(tag, tags):
"True if TAG is in TAGS. Ignore case."
return tag.lower() in [t.lower() for t in tags]
def addTags(tagstr, tags):
"Add tag if doesn't exist."
currentTags = parseTags(tags)
for tag in parseTags(tagstr):
if not findTag(tag, currentTags):
currentTags.append(tag)
return u", ".join(currentTags)
def deleteTags(tagstr, tags):
"Delete tag if exists."
currentTags = parseTags(tags)
for tag in parseTags(tagstr):
try:
currentTags.remove(tag)
except ValueError:
pass
return u", ".join(currentTags)
def stripHTML(s):
s = re.sub("<.*?>", "", s)
s = s.replace("&lt;", "<")
s = s.replace("&gt;", ">")
return s
def tidyHTML(html):
"Remove cruft like body tags and return just the important part."
# contents of body - no head or html tags
html = re.sub(".*<body.*?>(.*)</body></html>",
"\\1", html.replace("\n", u""))
# strip superfluous Qt formatting
html = re.sub("margin-top:\d+px; margin-bottom:\d+px; margin-left:\d+px; "
"margin-right:\d+px; -qt-block-indent:0; "
"text-indent:0px;", "", html)
html = re.sub("-qt-paragraph-type:empty;", "", html)
# collapse multiple spaces into one
html = re.sub(" +", " ", html)
# strip leading space in style statements, and remove if no contents
html = re.sub('style=" ', 'style="', html)
html = re.sub(' style=""', "", html)
# convert P tags into SPAN and/or BR
html = re.sub('<p( style=.+?)>(.*?)</p>', u'<span\\1>\\2</span><br>', html)
html = re.sub('<p>(.*?)</p>', u'\\1<br>', html)
html = re.sub('<br>$', u'', html)
# remove leading or trailing whitespace
html = re.sub('^ +', u'', html)
html = re.sub(' +$', u'', html)
return html
def genID(static=[]):
"Generate a random, unique 64bit ID."
# 23 bits of randomness, 41 bits of current time
# random rather than a counter to ensure efficient btree
t = long(time.time()*1000)
if not static:
static.extend([t, {}])
else:
if static[0] != t:
static[0] = t
static[1] = {}
while 1:
rand = random.getrandbits(23)
if rand not in static[1]:
static[1][rand] = True
break
x = rand << 41 | t
# turn into a signed long
if x >= 9223372036854775808L:
x -= 18446744073709551616L
return x
def ids2str(ids):
"""Given a list of integers, return a string '(int1,int2,.)'
The caller is responsible for ensuring only integers are provided.
This is safe if you use sqlite primary key columns, which are guaranteed
to be integers."""
return "(%s)" % ",".join([str(i) for i in ids])

228
ez_setup.py Normal file
View file

@ -0,0 +1,228 @@
#!python
"""Bootstrap setuptools installation
If you want to use setuptools in your package's setup.py, just include this
file in the same directory with it, and add this to the top of your setup.py::
from ez_setup import use_setuptools
use_setuptools()
If you want to require a specific version of setuptools, set a download
mirror, or use an alternate download directory, you can do so by supplying
the appropriate options to ``use_setuptools()``.
This file can also be run as a script to install or upgrade setuptools.
"""
import sys
DEFAULT_VERSION = "0.6c5"
DEFAULT_URL = "http://cheeseshop.python.org/packages/%s/s/setuptools/" % sys.version[:3]
md5_data = {
'setuptools-0.6b1-py2.3.egg': '8822caf901250d848b996b7f25c6e6ca',
'setuptools-0.6b1-py2.4.egg': 'b79a8a403e4502fbb85ee3f1941735cb',
'setuptools-0.6b2-py2.3.egg': '5657759d8a6d8fc44070a9d07272d99b',
'setuptools-0.6b2-py2.4.egg': '4996a8d169d2be661fa32a6e52e4f82a',
'setuptools-0.6b3-py2.3.egg': 'bb31c0fc7399a63579975cad9f5a0618',
'setuptools-0.6b3-py2.4.egg': '38a8c6b3d6ecd22247f179f7da669fac',
'setuptools-0.6b4-py2.3.egg': '62045a24ed4e1ebc77fe039aa4e6f7e5',
'setuptools-0.6b4-py2.4.egg': '4cb2a185d228dacffb2d17f103b3b1c4',
'setuptools-0.6c1-py2.3.egg': 'b3f2b5539d65cb7f74ad79127f1a908c',
'setuptools-0.6c1-py2.4.egg': 'b45adeda0667d2d2ffe14009364f2a4b',
'setuptools-0.6c2-py2.3.egg': 'f0064bf6aa2b7d0f3ba0b43f20817c27',
'setuptools-0.6c2-py2.4.egg': '616192eec35f47e8ea16cd6a122b7277',
'setuptools-0.6c3-py2.3.egg': 'f181fa125dfe85a259c9cd6f1d7b78fa',
'setuptools-0.6c3-py2.4.egg': 'e0ed74682c998bfb73bf803a50e7b71e',
'setuptools-0.6c3-py2.5.egg': 'abef16fdd61955514841c7c6bd98965e',
'setuptools-0.6c4-py2.3.egg': 'b0b9131acab32022bfac7f44c5d7971f',
'setuptools-0.6c4-py2.4.egg': '2a1f9656d4fbf3c97bf946c0a124e6e2',
'setuptools-0.6c4-py2.5.egg': '8f5a052e32cdb9c72bcf4b5526f28afc',
'setuptools-0.6c5-py2.3.egg': 'ee9fd80965da04f2f3e6b3576e9d8167',
'setuptools-0.6c5-py2.4.egg': 'afe2adf1c01701ee841761f5bcd8aa64',
'setuptools-0.6c5-py2.5.egg': 'a8d3f61494ccaa8714dfed37bccd3d5d',
}
import sys, os
def _validate_md5(egg_name, data):
if egg_name in md5_data:
from md5 import md5
digest = md5(data).hexdigest()
if digest != md5_data[egg_name]:
print >>sys.stderr, (
"md5 validation of %s failed! (Possible download problem?)"
% egg_name
)
sys.exit(2)
return data
def use_setuptools(
version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir,
download_delay=15
):
"""Automatically find/download setuptools and make it available on sys.path
`version` should be a valid setuptools version number that is available
as an egg for download under the `download_base` URL (which should end with
a '/'). `to_dir` is the directory where setuptools will be downloaded, if
it is not already available. If `download_delay` is specified, it should
be the number of seconds that will be paused before initiating a download,
should one be required. If an older version of setuptools is installed,
this routine will print a message to ``sys.stderr`` and raise SystemExit in
an attempt to abort the calling script.
"""
try:
import setuptools
if setuptools.__version__ == '0.0.1':
print >>sys.stderr, (
"You have an obsolete version of setuptools installed. Please\n"
"remove it from your system entirely before rerunning this script."
)
sys.exit(2)
except ImportError:
egg = download_setuptools(version, download_base, to_dir, download_delay)
sys.path.insert(0, egg)
import setuptools; setuptools.bootstrap_install_from = egg
import pkg_resources
try:
pkg_resources.require("setuptools>="+version)
except pkg_resources.VersionConflict, e:
# XXX could we install in a subprocess here?
print >>sys.stderr, (
"The required version of setuptools (>=%s) is not available, and\n"
"can't be installed while this script is running. Please install\n"
" a more recent version first.\n\n(Currently using %r)"
) % (version, e.args[0])
sys.exit(2)
def download_setuptools(
version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir,
delay = 15
):
"""Download setuptools from a specified location and return its filename
`version` should be a valid setuptools version number that is available
as an egg for download under the `download_base` URL (which should end
with a '/'). `to_dir` is the directory where the egg will be downloaded.
`delay` is the number of seconds to pause before an actual download attempt.
"""
import urllib2, shutil
egg_name = "setuptools-%s-py%s.egg" % (version,sys.version[:3])
url = download_base + egg_name
saveto = os.path.join(to_dir, egg_name)
src = dst = None
if not os.path.exists(saveto): # Avoid repeated downloads
try:
from distutils import log
if delay:
log.warn("""
---------------------------------------------------------------------------
This script requires setuptools version %s to run (even to display
help). I will attempt to download it for you (from
%s), but
you may need to enable firewall access for this script first.
I will start the download in %d seconds.
(Note: if this machine does not have network access, please obtain the file
%s
and place it in this directory before rerunning this script.)
---------------------------------------------------------------------------""",
version, download_base, delay, url
); from time import sleep; sleep(delay)
log.warn("Downloading %s", url)
src = urllib2.urlopen(url)
# Read/write all in one block, so we don't create a corrupt file
# if the download is interrupted.
data = _validate_md5(egg_name, src.read())
dst = open(saveto,"wb"); dst.write(data)
finally:
if src: src.close()
if dst: dst.close()
return os.path.realpath(saveto)
def main(argv, version=DEFAULT_VERSION):
"""Install or upgrade setuptools and EasyInstall"""
try:
import setuptools
except ImportError:
egg = None
try:
egg = download_setuptools(version, delay=0)
sys.path.insert(0,egg)
from setuptools.command.easy_install import main
return main(list(argv)+[egg]) # we're done here
finally:
if egg and os.path.exists(egg):
os.unlink(egg)
else:
if setuptools.__version__ == '0.0.1':
# tell the user to uninstall obsolete version
use_setuptools(version)
req = "setuptools>="+version
import pkg_resources
try:
pkg_resources.require(req)
except pkg_resources.VersionConflict:
try:
from setuptools.command.easy_install import main
except ImportError:
from easy_install import main
main(list(argv)+[download_setuptools(delay=0)])
sys.exit(0) # try to force an exit
else:
if argv:
from setuptools.command.easy_install import main
main(argv)
else:
print "Setuptools version",version,"or greater has been installed."
print '(Run "ez_setup.py -U setuptools" to reinstall or upgrade.)'
def update_md5(filenames):
"""Update our built-in md5 registry"""
import re
from md5 import md5
for name in filenames:
base = os.path.basename(name)
f = open(name,'rb')
md5_data[base] = md5(f.read()).hexdigest()
f.close()
data = [" %r: %r,\n" % it for it in md5_data.items()]
data.sort()
repl = "".join(data)
import inspect
srcfile = inspect.getsourcefile(sys.modules[__name__])
f = open(srcfile, 'rb'); src = f.read(); f.close()
match = re.search("\nmd5_data = {\n([^}]+)}", src)
if not match:
print >>sys.stderr, "Internal error!"
sys.exit(2)
src = src[:match.start(1)] + repl + src[match.end(1):]
f = open(srcfile,'w')
f.write(src)
f.close()
if __name__=='__main__':
if len(sys.argv)>2 and sys.argv[1]=='--md5update':
update_md5(sys.argv[2:])
else:
main(sys.argv[1:])

0
setup.cfg Normal file
View file

34
setup.py Normal file
View file

@ -0,0 +1,34 @@
from ez_setup import use_setuptools
use_setuptools()
from setuptools import setup, find_packages
import sys, os
import anki
setup(name='anki',
version=anki.version,
description='An intelligent spaced-repetition memory training library',
long_description="",
# Get strings from http://www.python.org/pypi?%3Aaction=list_classifiers
classifiers=[
'Intended Audience :: Developers',
'Intended Audience :: Education',
'License :: OSI Approved :: GNU General Public License (GPL)',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Topic :: Education',
'Topic :: Software Development :: Libraries :: Python Modules',
],
keywords='',
author='Damien Elmes',
author_email='anki@ichi2.net',
url='http://ichi2.net/anki/index.html',
license='GPLv3',
packages=find_packages(),
package_data={'anki':
['samples/*','locale/*/*/*']},
include_package_data=True,
zip_safe=False,
install_requires=[
],
)

0
tests/__init__.py Normal file
View file

219
tests/importing/test.mem Normal file
View file

@ -0,0 +1,219 @@
--- Mnemosyne Data Base --- Format Version 1 ---
(lp1
(imnemosyne.core.mnemosyne_core
StartTime
p2
(dp3
S'time'
p4
F1183141800
sba(lp5
(imnemosyne.core.mnemosyne_core
Category
p6
(dp7
S'active'
p8
I01
sS'name'
p9
V<default>
p10
sba(imnemosyne.core.mnemosyne_core
Category
p11
(dp12
S'active'
p13
I01
sS'name'
p14
Vfoo
p15
sba(imnemosyne.core.mnemosyne_core
Category
p16
(dp17
g13
I01
sg14
Vbaz, quux
p18
sbaa(lp19
(imnemosyne.core.mnemosyne_core
Item
p20
(dp21
S'a'
Vbar
p22
sS'last_rep'
p23
L34L
sS'ret_reps'
p24
I0
sS'cat'
p25
g16
sS'q'
Vfoo
p26
sS'grade'
p27
I0
sS'acq_reps'
p28
I1
sS'ret_reps_since_lapse'
p29
I0
sS'easiness'
p30
F2.5
sS'lapses'
p31
I0
sS'acq_reps_since_lapse'
p32
I1
sS'next_rep'
p33
L34L
sS'id'
p34
S'9f401476'
p35
sba(imnemosyne.core.mnemosyne_core
Item
p36
(dp37
S'a'
Vfoo
p38
sg23
L34L
sg24
I0
sg25
g6
sS'q'
Vbar
p39
sg27
I0
sg28
I1
sg29
I0
sg30
F2.5
sg31
I0
sg32
I1
sg33
L34L
sg34
S'a869958d'
p40
sba(imnemosyne.core.mnemosyne_core
Item
p41
(dp42
S'a'
Vquux
p43
sg23
L34L
sg24
I0
sg25
g11
sS'q'
Vbaz
p44
sg27
I5
sg28
I2
sg29
I0
sg30
F2.5
sg31
I0
sg32
I2
sg33
L35L
sg34
S'74651aa3'
p45
sba(imnemosyne.core.mnemosyne_core
Item
p46
(dp47
S'a'
Vbaz
p48
sg23
L34L
sg24
I0
sg25
g6
sS'q'
Vquux
p49
sg27
I0
sg28
I1
sg29
I0
sg30
F2.5
sg31
I0
sg32
I1
sg33
L34L
sg34
S'0bd8f10b'
p50
sba(imnemosyne.core.mnemosyne_core
Item
p51
(dp52
S'a'
Vcard
p53
sg23
L34L
sg24
I0
sg25
g11
sS'q'
Vonesided
p54
sg27
I0
sg28
I1
sg29
I0
sg30
F2.5
sg31
I0
sg32
I1
sg33
L34L
sg34
S'bb503cf1'
p55
sbaa.

BIN
tests/importing/test03.anki Normal file

Binary file not shown.

Binary file not shown.

BIN
tests/importing/test10.anki Normal file

Binary file not shown.

View file

@ -0,0 +1,9 @@
# this is a test file
食べる to eat
飲む to drink
テスト test
to eat 食べる
飲む to drink
遊ぶ
to play

7
tests/shared.py Normal file
View file

@ -0,0 +1,7 @@
def assertException(exception, func):
found = False
try:
func()
except exception:
found = True
assert found

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 545 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 580 B

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 644 B

139
tests/test_deck.py Normal file
View file

@ -0,0 +1,139 @@
# coding: utf-8
import nose, os
from tests.shared import assertException
from anki.errors import *
from anki import DeckStorage
from anki.db import *
from anki.models import FieldModel
from anki.stdmodels import JapaneseModel, BasicModel
newPath = None
newModified = None
## opening/closing
def test_new():
deck = DeckStorage.Deck()
assert deck.path
assert deck.engine
assert deck.modified
# for attachOld()
global newPath, newModified
newPath = deck.path
deck.save()
newModified = deck.modified
deck.close()
def test_attachNew():
path = "/tmp/test_attachNew"
try:
os.unlink(path)
except OSError:
pass
deck = DeckStorage.Deck(path)
deck.close()
del deck
os.unlink(path)
def test_attachOld():
deck = DeckStorage.Deck(newPath)
assert deck.modified == newModified
deck.close()
def test_attachReadOnly():
# non-writeable dir
assertException(DeckAccessError,
lambda: DeckStorage.Deck("/attachroot"))
# reuse tmp file from before, test non-writeable file
os.chmod(newPath, 0)
assertException(DeckAccessError,
lambda: DeckStorage.Deck(newPath))
os.chmod(newPath, 0666)
os.unlink(newPath)
def test_saveAs():
path = "/tmp/test_saveAs"
try:
os.unlink(path)
except OSError:
pass
deck = DeckStorage.Deck()
deck.addModel(BasicModel())
# add a card
f = deck.newFact()
f['Front'] = u"foo"; f['Back'] = u"bar"
deck.addFact(f)
# save in new deck
newDeck = deck.saveAs(path)
assert newDeck.cardCount() == 1
newDeck.close()
deck.close()
def test_factAddDelete():
deck = DeckStorage.Deck()
deck.addModel(BasicModel())
# set rollback point
deck.s.commit()
f = deck.newFact()
# empty fields
try:
deck.addFact(f)
except Exception, e:
pass
assert e.data['type'] == 'fieldEmpty'
# add a fact
f['Front'] = u"one"; f['Back'] = u"two"
deck.addFact(f)
assert len(f.cards) == 1
deck.rollback()
# try with two cards
f = deck.newFact()
f['Front'] = u"one"; f['Back'] = u"two"
f.model.cardModels[1].active = True
deck.addFact(f)
assert len(f.cards) == 2
# ensure correct order
c0 = [c for c in f.cards if c.ordinal == 0][0]
assert c0.question == u"one"
# now let's make a duplicate
f2 = deck.newFact()
f2['Front'] = u"one"; f2['Back'] = u"three"
try:
deck.addFact(f2)
except Exception, e:
pass
assert e.data['type'] == 'fieldNotUnique'
# try delete the first card
deck.deleteCard(f.cards[0].id)
# and the second should clear the fact
deck.deleteCard(f.cards[1].id)
def test_cardOrder():
deck = DeckStorage.Deck()
deck.addModel(JapaneseModel())
f = deck.newFact()
f['Expression'] = u'1'
f['Meaning'] = u'2'
deck.addFact(f)
card = deck.getCard()
# production should come first
assert card.cardModel.name == u"Production"
# if we rebuild the queue, it should be the same
deck.rebuildQueue()
card = deck.getCard()
assert card.cardModel.name == u"Production"
def test_modelAddDelete():
deck = DeckStorage.Deck()
deck.addModel(JapaneseModel())
deck.addModel(JapaneseModel())
f = deck.newFact()
f['Expression'] = u'1'
f['Meaning'] = u'2'
deck.addFact(f)
assert deck.cardCount() == 2
deck.deleteModel(deck.currentModel)
assert deck.cardCount() == 0
deck.s.refresh(deck)

80
tests/test_exporting.py Normal file
View file

@ -0,0 +1,80 @@
# coding: utf-8
import nose, os, tempfile
import anki
from anki import DeckStorage
from anki.exporting import *
from anki.stdmodels import *
deck = None
ds = None
testDir = os.path.dirname(__file__)
def setup1():
global deck
deck = DeckStorage.Deck()
deck.addModel(BasicModel())
deck.currentModel.cardModels[1].active = True
f = deck.newFact()
f['Front'] = u"foo"; f['Back'] = u"bar"; f.tags = u"tag, tag2"
deck.addFact(f)
f = deck.newFact()
f['Front'] = u"baz"; f['Back'] = u"qux"
deck.addFact(f)
##########################################################################
@nose.with_setup(setup1)
def test_export_anki():
oldTime = deck.modified
e = AnkiExporter(deck)
newname = unicode(tempfile.mkstemp()[1])
os.unlink(newname)
e.exportInto(newname)
assert deck.modified == oldTime
# connect to new deck
d2 = DeckStorage.Deck(newname)
assert d2.cardCount() == 4
# try again, limited to a tag
newname = unicode(tempfile.mkstemp()[1])
os.unlink(newname)
e.limitTags = ['tag']
e.exportInto(newname)
d2 = DeckStorage.Deck(newname)
assert d2.cardCount() == 2
@nose.with_setup(setup1)
def test_export_textcard():
e = TextCardExporter(deck)
f = unicode(tempfile.mkstemp()[1])
os.unlink(f)
e.exportInto(f)
e.includeTags = True
e.exportInto(f)
# # test speed
# newname = unicode(tempfile.mkstemp()[1])
# os.unlink(newname)
# d = DeckStorage.Deck("/home/resolve/speed.db")
# import time; t = time.time()
# e = TextCardExporter(d)
# e.exportInto(newname)
# print time.time() - t
@nose.with_setup(setup1)
def test_export_textfact():
e = TextFactExporter(deck)
f = unicode(tempfile.mkstemp()[1])
os.unlink(f)
e.exportInto(f)
e.includeTags = True
e.exportInto(f)
# # test speed
# newname = unicode(tempfile.mkstemp()[1])
# os.unlink(newname)
# d = DeckStorage.Deck("/home/resolve/speed.db")
# import time; t = time.time()
# e = TextFactExporter(d)
# e.exportInto(newname)
# print time.time() - t

31
tests/test_features.py Normal file
View file

@ -0,0 +1,31 @@
# coding: utf-8
import nose, os
from tests.shared import assertException
from anki.errors import *
from anki import DeckStorage
from anki.db import *
from anki.stdmodels import JapaneseModel, MandarinModel, CantoneseModel
def test_japanese():
deck = DeckStorage.Deck()
deck.addModel(JapaneseModel())
f = deck.newFact()
f['Expression'] = u'了解'
f.onKeyPress(f.fields[0], f.fields[0].value)
assert f['Reading'] == u'りょうかい'
def test_chinese():
deck = DeckStorage.Deck()
deck.addModel(MandarinModel())
f = deck.newFact()
f['Expression'] = u'食べる'
f.onKeyPress(f.fields[0], f.fields[0].value)
assert f['Reading'] == u"{shí,sì,yì}"
deck = DeckStorage.Deck()
deck.addModel(CantoneseModel())
f = deck.newFact()
f['Expression'] = u'食べる'
f.onKeyPress(f.fields[0], f.fields[0].value)
assert f['Reading'] == u"{ji6,sik6,zi6}"

64
tests/test_importing.py Normal file
View file

@ -0,0 +1,64 @@
# coding: utf-8
import nose, os, shutil
from tests.shared import assertException
from anki.errors import *
from anki import DeckStorage
from anki.importing import anki03, anki10, csv, mnemosyne10
from anki.stdmodels import BasicModel
from anki.db import *
testDir = os.path.dirname(__file__)
def test_csv():
deck = DeckStorage.Deck()
deck.addModel(BasicModel())
file = unicode(os.path.join(testDir, "importing/text-2fields.txt"))
i = csv.TextImporter(deck, file)
i.doImport()
# three problems - missing front, missing back, dupe front
assert len(i.log) == 3
assert i.total == 4
deck.s.close()
def test_mnemosyne10():
deck = DeckStorage.Deck()
deck.addModel(BasicModel())
file = unicode(os.path.join(testDir, "importing/test.mem"))
i = mnemosyne10.Mnemosyne10Importer(deck, file)
i.doImport()
assert i.total == 5
deck.s.close()
def test_anki03():
deck = DeckStorage.Deck()
file = unicode(os.path.join(testDir, "importing/test03.anki"))
i = anki03.Anki03Importer(deck, file)
i.doImport()
assert len(i.log) == 0
assert i.total == 2
deck.s.close()
def test_anki10():
# though these are not modified, sqlite updates the mtime, so copy to tmp
# first
file_ = unicode(os.path.join(testDir, "importing/test10.anki"))
file = "/tmp/test10.anki"
shutil.copy(file_, file)
file2_ = unicode(os.path.join(testDir, "importing/test10-2.anki"))
file2 = "/tmp/test10-2.anki"
shutil.copy(file2_, file2)
deck = DeckStorage.Deck()
i = anki10.Anki10Importer(deck, file)
i.doImport()
assert i.total == 2
deck.s.rollback()
# import a deck into itself - 10-2 is the same as test10, but with one
# card answered and another deleted. nothing should be synced to client
deck = DeckStorage.Deck(file)
i = anki10.Anki10Importer(deck, file2)
i.doImport()
assert i.total == 0
deck.s.rollback()

22
tests/test_stdmodels.py Normal file
View file

@ -0,0 +1,22 @@
# coding: utf-8
import nose, os
from tests.shared import assertException
from anki.errors import *
from anki import DeckStorage
from anki.db import *
from anki.stdmodels import *
def test_stdmodels():
# test all but basicmodel
deck = DeckStorage.Deck()
deck.addModel(JapaneseModel())
deck = DeckStorage.Deck()
deck.addModel(EnglishModel())
deck = DeckStorage.Deck()
deck.addModel(HeisigModel())
deck = DeckStorage.Deck()
deck.addModel(CantoneseModel())
deck = DeckStorage.Deck()
deck.addModel(MandarinModel())

276
tests/test_sync.py Normal file
View file

@ -0,0 +1,276 @@
# coding: utf-8
import nose, os, tempfile, shutil, time
from tests.shared import assertException
from anki.errors import *
from anki import DeckStorage
from anki.db import *
from anki.stdmodels import BasicModel, JapaneseModel
from anki.sync import SyncClient, SyncServer, HttpSyncServer, HttpSyncServerProxy
from anki.stats import dailyStats, globalStats
from anki.facts import Fact
from anki.cards import Card
from anki.models import FieldModel
from anki.media import rebuildMediaDir
#import psyco; psyco.profile()
# Local tests
##########################################################################
deck1=None
deck2=None
client=None
server=None
def setup_local(loadDecks=None):
global deck1, deck2, client, server
if loadDecks:
deck1 = DeckStorage.Deck(loadDecks[0])
deck2 = DeckStorage.Deck(loadDecks[1])
else:
deck1 = DeckStorage.Deck()
deck1.addModel(BasicModel())
deck1.currentModel.cardModels[1].active = True
deck1.newCardOrder = 1
f = deck1.newFact()
f['Front'] = u"foo"; f['Back'] = u"bar"
deck1.addFact(f)
deck2 = DeckStorage.Deck()
deck2.addModel(BasicModel())
deck2.currentModel.cardModels[1].active = True
f = deck2.newFact()
f['Front'] = u"baz"; f['Back'] = u"qux"
deck2.addFact(f)
deck2.newCardOrder = 1
client = SyncClient(deck1)
server = SyncServer(deck2)
client.setServer(server)
def teardown():
pass
@nose.with_setup(setup_local, teardown)
def test_localsync_diffing():
assert deck1.cardCount() == 2
assert deck2.cardCount() == 2
lsum = client.summary(deck1.lastSync)
rsum = server.summary(deck1.lastSync)
result = client.diffSummary(lsum, rsum, 'cards')
assert len(result[0]) == 2
assert len(result[1]) == 0
assert len(result[2]) == 2
assert len(result[3]) == 0
result = client.diffSummary(lsum, rsum, 'facts')
assert len(result[0]) == 1
assert len(result[1]) == 0
assert len(result[2]) == 1
assert len(result[3]) == 0
@nose.with_setup(setup_local, teardown)
def test_localsync_deck():
# deck two was modified last
assert deck2.modified > deck1.modified
d2mod = deck2.modified
assert deck1.lastSync == 0 and deck2.lastSync == 0
client.sync()
assert deck1.modified == deck2.modified
assert deck1.lastSync == deck1.modified
assert deck1.lastSync == deck2.lastSync
# ensure values are being synced
deck1.lowPriority += u",foo"
deck1.updateAllPriorities()
deck1.setModified()
client.sync()
assert "foo" in deck2.lowPriority
assert deck1.modified == deck2.modified
assert deck1.lastSync == deck2.lastSync
deck2.description = u"newname"
deck2.setModified()
client.sync()
assert deck1.description == u"newname"
# the most recent change should take precedence
deck1.description = u"foo"
deck1.setModified()
deck2.description = u"bar"
deck2.setModified()
client.sync()
assert deck1.description == "bar"
# answer a card to ensure stats & history are copied
c = deck1.getCard()
deck1.answerCard(c, 4)
client.sync()
assert dailyStats(deck2.s).reps == 1
assert globalStats(deck2.s).reps == 1
assert deck2.s.scalar("select count(id) from reviewHistory") == 1
@nose.with_setup(setup_local, teardown)
def test_localsync_models():
client.sync()
# add a model
deck1.addModel(JapaneseModel())
assert len(deck1.models) == 3
assert len(deck2.models) == 2
client.sync()
assert len(deck2.models) == 3
assert deck1.currentModel.id == deck2.currentModel.id
# delete the recently added model
deck2.deleteModel(deck2.currentModel)
assert len(deck2.models) == 2
client.sync()
assert len(deck1.models) == 2
assert deck1.currentModel.id == deck2.currentModel.id
# make a card model inactive
assert deck1.currentModel.cardModels[1].active == True
deck2.currentModel.cardModels[1].active = False
deck2.currentModel.setModified()
deck2.setModified()
client.sync()
assert deck1.currentModel.cardModels[1].active == False
# remove a card model
deck1.deleteCardModel(deck1.currentModel,
deck1.currentModel.cardModels[1])
deck1.currentModel.setModified()
deck1.setModified()
assert len(deck1.currentModel.cardModels) == 1
client.sync()
assert len(deck2.currentModel.cardModels) == 1
# add a field
c = deck1.getCard()
deck1.addFieldModel(c.fact.model, FieldModel(u"yo"))
deck1.s.refresh(c.fact)
assert len(c.fact.fields) == 3
assert c.fact['yo'] == u""
client.sync()
c2 = deck2.s.query(Card).get(c.id)
deck2.s.refresh(c2.fact)
assert c2.fact['yo'] == u""
# remove the field
assert "yo" in c2.fact.keys()
deck2.deleteFieldModel(c2.fact.model, c2.fact.model.fieldModels[2])
deck2.s.refresh(c2.fact)
assert "yo" not in c2.fact.keys()
client.sync()
deck1.s.refresh(c.fact)
assert "yo" not in c.fact.keys()
# rename a field
assert u"Front" in c.fact.keys()
deck1.renameFieldModel(deck1.currentModel,
deck1.currentModel.fieldModels[0],
u"Sideways")
client.sync()
assert deck2.currentModel.fieldModels[0].name == u"Sideways"
@nose.with_setup(setup_local, teardown)
def test_localsync_factsandcards():
assert deck1.factCount() == 1 and deck1.cardCount() == 2
assert deck2.factCount() == 1 and deck2.cardCount() == 2
client.sync()
assert deck1.factCount() == 2 and deck1.cardCount() == 4
assert deck2.factCount() == 2 and deck2.cardCount() == 4
# ensure the fact was copied across
f1 = deck1.s.query(Fact).first()
f2 = deck1.s.query(Fact).get(f1.id)
f1['Front'] = u"myfront"
f1.setModified()
deck1.setModified()
client.sync()
f2 = deck1.s.query(Fact).get(f1.id)
assert f2['Front'] == u"myfront"
deck1.rebuildQueue()
deck2.rebuildQueue()
c1 = deck1.getCard()
c2 = deck2.getCard()
assert c1.id == c2.id
@nose.with_setup(setup_local, teardown)
def test_localsync_threeway():
# deck1 (client) <-> deck2 (server) <-> deck3 (client)
deck3 = DeckStorage.Deck()
client2 = SyncClient(deck3)
server2 = SyncServer(deck2)
client2.setServer(server2)
client.sync()
client2.sync()
# add a new question
f = deck1.newFact()
f['Front'] = u"a"; f['Back'] = u"b"
cards = deck1.addFact(f)
card = cards[0]
client.sync()
assert deck1.cardCount() == 6
assert deck2.cardCount() == 6
# check it propagates from server to deck3
client2.sync()
assert deck3.cardCount() == 6
# delete a card on deck1
deck1.deleteCard(card.id)
client.sync()
assert deck1.cardCount() == 5
assert deck2.cardCount() == 5
# make sure the delete is now propagated from the server to deck3
client2.sync()
assert deck3.cardCount() == 5
def test_localsync_media():
tmpdir = "/tmp/media-tests"
try:
shutil.rmtree(tmpdir)
except OSError:
pass
shutil.copytree(os.path.join(os.getcwd(),
"tests/syncing/media-tests"),
tmpdir)
deck1anki = os.path.join(tmpdir, "1.anki")
deck2anki = os.path.join(tmpdir, "2.anki")
deck1media = os.path.join(tmpdir, "1.media")
deck2media = os.path.join(tmpdir, "2.media")
setup_local((deck1anki, deck2anki))
assert len(os.listdir(deck1media)) == 2
assert len(os.listdir(deck2media)) == 1
client.sync()
assert len(os.listdir(deck1media)) == 3
assert len(os.listdir(deck2media)) == 3
assert deck1.s.scalar("select count(1) from media") == 3
assert deck2.s.scalar("select count(1) from media") == 3
# check delete
os.unlink(os.path.join(deck1media, "22161b29b0c18e068038021f54eee1ee.png"))
rebuildMediaDir(deck1)
client.sync()
assert deck1.s.scalar("select count(1) from media") == 2
assert deck2.s.scalar("select count(1) from media") == 2
# Remote tests
##########################################################################
# a replacement runCmd which just calls our server directly
def runCmd(action, *args, **kargs):
#print action, kargs
return server.unstuff(apply(getattr(server, action), tuple(args) +
tuple(kargs.values())))
def setup_remote():
setup_local()
global client, server
proxy = HttpSyncServerProxy("test", "foo")
client = SyncClient(deck1)
client.setServer(proxy)
proxy.deckName = "test"
proxy.runCmd = runCmd
server = HttpSyncServer()
server.deck = deck2
server.decks = {"test": (deck2.modified, 0)}
@nose.with_setup(setup_remote, teardown)
def test_remotesync_fromserver():
# deck two was modified last
assert deck2.modified > deck1.modified
client.sync()
assert deck2.modified == deck1.modified
@nose.with_setup(setup_remote, teardown)
def test_remotesync_toserver():
deck1.setModified()
client.sync()
assert deck2.modified == deck1.modified

18
tests/test_utils.py Normal file
View file

@ -0,0 +1,18 @@
# coding: utf-8
import nose, os
from tests.shared import assertException
from anki.errors import *
from anki.facts import *
from anki import DeckStorage
from anki.utils import *
def test_tags():
card = "one, two"
fact = "two,three, two"
cmodel = "four"
assert (sorted(parseTags(mergeTags(card, fact, cmodel))) ==
['four', 'one', 'three', 'two'])

8
tools/tests.sh Executable file
View file

@ -0,0 +1,8 @@
#!/bin/bash
if [ -d 'locale' ]; then
dir=..
else
dir=.
fi
(cd $dir && nosetests -vs $@)

30
tools/translate.sh Executable file
View file

@ -0,0 +1,30 @@
#!/bin/bash
#
# update translation files
#
if [ ! -d "locale" ]
then
echo "Please run this from the anki module directory"
exit
fi
allPyFiles=libanki.files
echo "Generating translations.."
for i in *.py features/*.py
do
echo $i >> $allPyFiles
done
xgettext -s --no-wrap --files-from=$allPyFiles --output=locale/messages.pot
for file in locale/*.po
do
echo -n $file
msgmerge -s --no-wrap $file locale/messages.pot > $file.new && mv $file.new $file
outdir=$(echo $file | \
perl -pe 's%locale/libanki_(.*)\.po%locale/\1/LC_MESSAGES%')
outfile="$outdir/libanki.mo"
mkdir -p $outdir
msgfmt $file --output-file=$outfile
done
rm $allPyFiles